html 🔗
标签元素的有序集合。
javascript 🔗
js 语言的 runtime 有浏览器和 nodejs。
js 的核心语法精简, 只包括两部分: 基本的语法构造(操作符,控制语句),标准库(Array, Date, Math 等)。
此外是宿主环境提供额外的 api,浏览器提供三大类: 浏览器控制类(操作浏览器),DOM 类,Web 类(实现互联网的功能)。
- 基本语法
- 标准库
- 浏览器 api
- DOM
核心点 🔗
new 关键字形成的原型链: 实例.__proto__ == 构造函数.prototype。
原型,原型链,对象。
prototype 和 __proto__ 的区别?
prototype是函数独有,决定用 new 调用该函数时,新对象的 __proto__ 指向哪里。proto是所有对象都有(包括函数也是对象),指向该对象的原型对象。
function Foo() {}
Foo.prototype.sayA = function () {
console.log("say A");
};
const a = new Foo();
console.log(Foo.prototype); // {sayA: f, constructor: f}
console.log(a.__proto === Foo.prototype); // true
// constructor 的作用就是 记录“这个对象是由哪个构造函数创建的”
console.log(a.contructor === Foo); // true
console.log(Foo.prototype.constructor === Foo); // true
- 函数有
prototype - 对象有
__proto__属性- ES6 推荐使用 Object.getPrototypeOf 获取原型对象。
- 实例.proto === 构造函数.prototype
所有对象都有constructor 和__proto__。__proto__ 和浏览器控制台里看到的[[Prototype]]是一个意思。constructor 是一个属性,默认指向创建该对象原型的构造函数。
function Foo() {}
const foo = new Foo();
// foo.constructor 实际上是通过原型链拿到的
console.log(foo.constructor); // Foo
console.log(Foo.prototype.constructor); // Foo
__proto__ 是实例对象的属性,指向它的构造函数的 prototype,用于原型链查找。
A instanceof B 判断B.prototype是否在 A 的原型链上。A.__proto__,以及A.__proto__.__proto__ 是否等于B.prototype。
Object instanceof Object; //true, Object是构造函数, Object.__proto__ == Function.prototype, Function.prototype.__proto == Object.prototype.
Function instanceof Function; //true, Function.__proto == Function.prototype
Object instanceof Function; //true, Object.__proto == Function.prototype
Function instanceof Object; //true, Function.__proto == Function.prototype, Function.prototype.__proto == Object.prototype.
Object.prototype是原型链的起点, 任何原型都源自它。Object.prototype.__proto == null。
Function.prototype是仅次于Object.prototype的存在,它是内置构造函数的原型,任何构造函数都源自它。 所有函数对象(包括 Function 自己)都是Function构造函数的实例, 所以函数对象的__proto__都指向Function.prototype。因此Function.__proto__ === Function.prototype。
所有对象都有 constructor 吗? 在 JavaScript 中,每一个函数(包括 class 定义出来的类)在创建时都会自动带上一个 prototype 对象,而 prototype 上会有一个默认的 constructor 属性。 console.log(Foo.prototype.constructor === Foo); // true 。
Object.create(null) 创建的“纯净对象”,它的原型是 null,没有 constructor。
function Parent() {}
function Child() {}
// 修改 prototype 时要注意 constructor 可能会丢失
Child.prototype = new Parent(); // 继承
console.log(Child.prototype.constructor === Parent); // true ❌ 被覆盖了
// 知道一个对象是由哪个构造函数生成的
// 手动修正
Child.prototype.constructor = Child;
Object.create(null) 对象, 原型, constructor。
创建一个 新对象,并把它的 __proto__(内部 [[Prototype]])指向 proto。
const obj = Object.create(null); // 完全干净的对象
// 没有继承 Object.prototype 上的东西(如 toString、hasOwnProperty 等)
console.log(obj.__proto__ === null); // true
console.log(obj.constructor); // undefined
如何区分普通函数与构造函数? 🔗
<body>
<script>
function a() {
console.log("a is func");
}
// 函数有二义性,既可以当普通函数调用,也可以当构造函数调用。
a();
let instance = new a();
console.log(instance);
</script>
</body>
解决函数二义性的问题, ES6 中使用 class 定义构造函数。
class A {
constructor() {
console.log("class A's constructor function");
}
}
// right
let instance = new A();
// error
A(); // Uncaught TypeError: Class constructor A cannot be invoked without 'new'
还可以在函数内部可以使用new.target来区别。
function A() {
if (new.target) {
console.log("A is constructor function");
} else {
console.log("A is normal function");
}
}
A();
new A();
如何通过点操作符寻找函数和属性 🔗
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function () {
console.log("hi, I am " + this.name);
};
const a = new Person("Jane");
a.sayHi(); // ?. 相当于。 Person.prototype.sayHi.call(a)
//取属性, 取出 sayHi 这个函数引用. const fn = a.sayHi, 此时 fn === Person.prototype.sayHi
// 调用阶段 → 把 this 绑定为 a, fn.call(a)
function getProperty(obj, prop) {
while (obj != null) {
if (obj.hasOwnProperty(prop)) {
// 先看对象自身是否有该属性
return obj[prop];
}
// 如果没有,就去它的 原型对象(prototype) 上找
obj = Object.getPrototypeOf(obj);
// obj = obj.__proto__
}
return undefined;
}
| 调用形式 | this 指向 |
|---|---|
obj.fn() |
obj |
fn() |
undefined(严格模式)或 window(非严格模式) |
fn.call(obj) / fn.apply(obj) |
手动绑定为 obj |
fn.bind(obj) |
返回一个永久绑定 this 的新函数 |
new Fn() |
this 指向新创建的实例对象 |
基本语法 🔗
js runtime 时的工作方式是: 先解析代码获取所有声明的变量,再一行一行执行。var 出现变量提升。
数据的类型 :null 与 undefined。
查看值的类型: typeof, instanceof, Object.prototype.toString。
undefined 此初无定义,声明但没初始化。
null 空值。
undefined, null, false, 0, "", '', NaN 可以转为 false。空数组,空对象都是 true。
Object类型。
典型的面向对象编程语言(c++, java) 有类的概念。类是对象的模版,对象是类的实例。 但 javascript 的对象体系里,不是基于类的,而是基于构造函数(constructor)和原型链(prototype)。
var Vehicle = function () {
this.price = 100; // 函数内部使用了this
};
var v = new Vehicle(); // 生成对象时使用了new,忘记带new时不会生成实例对象。
this 会动态切换。js 提供了 call,apply,bind 三个方法来切换/绑定 this 的指向。call 和 apply 是类似的,是立即调用函数,并显式指定 this。 bind 返回一个永久绑定了 this 的新函数(不会立即调用)。
js 中所有对象都有自己的原型对象。原型对象的作用是定义所有实例对象共享的属性和方法。
普通函数与箭头函数的区别? 🔗
在 this 绑定、原型区别:箭头函数是没有自己的 this / prototype / arguments 的函数表达式,它的 this 由定义时的词法作用域决定;普通函数是可构造的,有自己的 this / prototype / arguments,this 在调用时绑定。
前端的防抖和节流的本质是利用闭包保存跨调用的状态,否则就无法记住“上一次”发生了什么。
// 防抖
function debounce(event, wait) {
let timer = null;
return function(..args) {
clearTimeout(timer);
time = setTimeout(()=>{
event.apply(this, args);
}, wait);
}
}
// 节流
function throttle(event, wait) {
let pre = 0,
timer = null;
return function (...args) {
if (new Date() - pre > wait) {
clearTimeout(timer);
timer = null;
pre = new Date();
event.apply(this, args);
} else {
timer = setTimeout(() => {
event.apply(this, args);
}, wait);
}
};
}
function throttle(func, delay) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= delay) {
lastTime = now;
func.apply(this, ...args);
}
};
}
window.addEventListener(
"scroll",
throttle(() => {
console.log("scroll event..");
}, 200)
); // 每200s 触发一次。
IIFE 立即执行函数的表达式 🔗
(function () {})();
why? 需要隔离作用域。
javascript 中的数组 🔗
原地改变数组的方法:。。 不改原数组返回新数组的方法:。。
数组的索引中有哪些隐式类型转换? 🔗
JavaScript 数组的 “索引” 本质上是对象的属性名。而 JavaScript 中对象的属性名只能是字符串或 Symbol。
// 数组是一种特殊的对象,其 “索引” 本质上是对象的属性。
const arr = [10, 20];
// 等价于(简化理解):
const arr = {
0: 10,
1: 20,
length: 2,
};
数组的索引可以是字符串吗? 除 Symbol 类型之外最终都是字符串,即使用数字作为索引也会转为字符串。
Symbol 类型 🔗
ES6 引入的基础类型。 唯一性。
const s1 = Symbol("x");
const s2 = Symbol("x");
console.log(s1 === s2); // false
// 开发者 A 创建一个 Symbol 作为属性名
const idSymbolA = Symbol("id");
const user = {
name: "张三",
[idSymbolA]: 1001, // 用 Symbol 作为键,避免冲突
};
// 开发者 B 也创建一个 Symbol(即使描述符相同,也是不同的键)
const idSymbolB = Symbol("id");
user[idSymbolB] = "user_1001"; // 不会覆盖 A 的属性
console.log(user[idSymbolA]); // 1001(A 的值保留)
console.log(user[idSymbolB]); // 'user_1001'(B 的值保留)
不可枚举。
const obj = {
[s1]: "value",
name: "foo",
};
console.log(Object.keys(obj)); //['name'] , (Symbol 属性未被遍历)
// 可以模拟私有属性,但是Symbol 属性并非绝对私有,可通过 Object.getOwnPropertySymbols() 方法获取。
如何实现私有属性? 🔗
let obj = {
__name: "xyz",
getName: function () {
return this.__name;
},
};
console.log(obj.getName()); // 打印xyz
console.log(obj.__name); //也 打印xyz
那怎么实现私有属性呢?
// 一,使用Symbol变量实现私有属性
let nameSymbol = Symbol("name");
let obj = {
[nameSymbol]: "xyz";
getName: function() {
return this[nameSymbol];
}
}
console.log(obj.getName()); // 打印 xyz
console.log(obj["name"]); // undefined
// 缺点
console.log(obj[nameSymbol]); // 还是可以打印 xyz
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(name)]
// 二,使用ES12中引入的语法,#前缀的变量名
class Myclass {
#privateField = "xyz";
getPrivateField() {
return this.#privateField;
}
}
let instance = new Myclass();
console.log(instance.getPrivateField());
console.log(instance.#privateField);
方括号和点操作符访问属性时有啥区别? 🔗
[], .。 静态属性和动态属性的理解。
静态属性的访问: 点操作符(需要是有效的 js 标识符,不能是数字了, obj.1=2, obj[1]=2), 属性名为硬编码。 动态属性的访问: 方括号运算符,属性名在运行时计算得出。
完整之后,先整理清楚 js 这部分内容。
小红书上的面试题,强行扫一遍。
javascript 中的循环 🔗
迭代方法: forEach, map, filter, reduce 修改原数组: push, pop, shift, unshift, splice, sort, reverse. 返回新数据: slice, concat, map,filter, reduce
for, for...in, forEach, map。
for (let i = 0; i < 10; i++) {
console.log(i);
}
var obj = { a: 1, b: 3, c: 2 };
for (let key in obj) {
console.log("obj." + key + " = " + obj[key]);
}
闭包 🔗
为什么需要闭包? 数据封装和创建私有变量。
可以追踪到 ingition 中对于闭包的实现。利用 d8 查看闭包的字节码。
实例方法和原型方法的区别? 🔗
function Person() {
Person.say = function () {
console.log("a");
};
this.say = function () {
console.log("b");
};
}
Person.prototype.say = function () {
console.log("c");
};
Person.say = function () {
console.log("d");
};
Person.say(); // 调用静态方法, d
var obj = new Person(); // 此时才执行构造函数,覆盖了静态方法say,增加实例方法
obj.say(); // b
Person.say(); //a
什么是属性描述符? 🔗
用属性描述符改写构造函数
function test(a, b, c) {
this.a = a;
this.b = b;
this.c = c;
// 对属性的访问和修改进行拦截
Object.defineProperty(this, "sum", {
get: function () {
return this.a + this.b + this.c;
},
});
// 如果改成这样? 闭包中保存的初始值
Object.defineProperty(this, "sum", {
get: function () {
return a + b + c;
},
});
}
let obj = new test(1, 2, 3);
柯里化 🔗
function sum(a, b, c) {
console.log(a + b + c);
}
// 高阶函数,参数收集
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function (...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
let _sum = curry(sum);
let fa = _sum(2);
let fb = _fa(3);
fb(4); // print: 9
组合函数 🔗
从右到左执行
function addOne(x) {
return x + 1;
}
function double(x) {
return x * 2;
}
function compose(fun1, fun2) {
return function (x) {
return fun1(fun2(x));
};
}
let combinedFunction = compose(double, addOne);
let result = combinedFunction(5);
console.log(result);
function compose1(...fns) {
return function (x) {
return fns.reduceRight(function (arg, fn) {
return fn(arg);
}, x);
};
}
实现管道函数 🔗
从左到右执行。
function pipe(...fns) {
return function (x) {
return fns.reduce(function (acc, fn) {
return fn(acc);
}, x);
};
}
func func1(x) {
return x + 1;
}
func func2(x) {
return x * 3;
}
const pipeline = pipe(func1, func2);
const output = pipeline(5);
console.log("output=", output); // 18
Object.freeze 和 const 🔗
const 是不允许对象再分配,Object.freeze 是不允许对象(如果嵌套了对象依然可以改)被修改。
function deepFreeze(obj) {
let propNames = Object.getOwnPropertyNames(obj);
propNames.forEach(function (name) {
let child = obj[name];
if (typeof child == "object" && child !== null) deepFreeze(child);
});
return Object.freeze(obj);
}
变量遮挡,变量提升 🔗
var test = "hello, world";
(function test() {
test = "xyz"; // 函数的名称在函数内部不能修改。
console.log(test); // 打印test函数,而非xyz。
})();
var a = 1;
function foo() {
// 变量提升,变量声明和函数声明会被提升到作用域的顶部,赋值操作符不会被提升
// var a;
console.log("first a", a); // undefined
var a = 3; // a = 3;
console.log("second a", a); // 3
}
console.log("third a", a); // 1
foo();
如何利用迭代器实现解构 🔗
let generator = function* () {
yield "1";
yield "2";
}; // 生成器
// 调用生成器返回迭代器
let iterator = generator();
console.log(iterator.next()); // 一个封装了value和done的对象
console.log(iterator.next());
let obj = { x: 1, y: 2 };
obj[Symbol.iterator] = function* () {
yield this.x;
yield this.y;
};
// let iterator = obj[Symbol.iterator];
// let x = iterator.next().value;
// let y = iterator.next().value;
let [x, y] = obj; // 解构的本质是调用迭代器获取值
console.log(x, y);
Object.is, ===, == 区别 🔗
==非严格相等有隐式类型转换。
console.log(null == undefined);
console.log(NaN == NaN);
Object.is 和=== 不会进行类型转换
Object.is(NaN, NaN); // true;
Object.is(+0, -0); // false;
console.log(NaN === NaN); //false;
console.log(+0 === -0); //true;
如何使用 Proxy 实现函数的链式调用? 🔗
const target = {
value: 42,
};
const handler = {
get: function (target, property) {
console.log(target, property);
return target[property];
},
};
const proxy = new Proxy(target, handler);
console.log(proxy.value);
function increase(a) {
return a + 1;
}
function decrease(a) {
return a - 1;
}
function double(a) {
return 2 * a;
}
function chain(value) {
const handler = {
get: function (obj, prop) {
if (prop === "end") {
return obj.value;
}
if (typeof window[prop] === "function") {
obj.value = window[prop](obj.value);
return proxy;
}
return obj[prop];
},
};
// {value} -> obj
const proxy = new Proxy({ value }, handler);
return proxy;
}
console.log(chain(3).increase.double.end);
console.log(chain(2).decrease.double.end);
数组的扁平化 🔗
Array.prototype.customFlatten = function () {
let flaten = [];
for (let item of this) {
if (Array.isArray(item)) {
flaten = flaten.concat(item.customFlatten);
} else {
flaten.push(item);
}
}
return flaten;
};
let arr = [
[1, 2, 3],
[3, 4, 5],
[3, 4, [6, 9, 8]],
];
console.log(arr.customFlatten());
forEach 的实现原理 🔗
循环中删除元素,会影响循环次数吗?
Array.prototype.myForEach = function (callbackfn, thisArg) {
if (this === null || this === undefined) {
throw new TypeError("this is null or undefined");
}
let O = this;
let len = O.length;
if (typeof callbackfn !== "function") {
throw new TypeError("callback is not function");
}
for (let k = 0; k < len; k++) {
console.log("k=", k, k in O);
if (k in O) {
let kValue = O[k];
callbackfn.call(thisArgs, kValue, k, 0);
}
}
return undefined;
};
let numbesOne = [1, 2, 3];
numbersOne.forEach((number, index) => {
console.log(number); // 循环次数已经确定了, 1, 2, 3
numbersOne.push(number + 10);
});
console.log("numbersOne res=", numbersOne); // [1,2,3,11,12,13]
let numbersTwo = [1, 2, 3];
numbersTwo.forEach((number, index) => {
console.log(number); // 1, 2
numbersTwo.pop();
});
console.log("numbersTwo res=", numbersTwo); // [1];
keyof 操作符可获取某种类型的所有键,返回的是 key 的 name 的联合类型。
for 循环 🔗
const queue = [x];
for (const asset of queue) {
//
asset.deps.forEach((path) => {
const child = createAsset();
queue.push(child); // for循环里修改数据的长度。
});
}
Object 和 map 的区别? 🔗
key 的类型不同。 object 的 key 是字符串或者 Symbol, key 不是插入排序。 Map 的 key 可以是任意类型的值,key 是插入排序。
判断 this 的指向 🔗
var length = 1;
function fun() {
console.log(this.length);
}
let arr = [fun, "a", "b"];
arr[0](); // 3, this 指向arr, this.length=3
let fun2 = arr[0]; //this执行window, ,1
fun2();
void 0永远可以稳定返回undefined, 但是字面量 undefined 可以人为被修改。
- this 的值什么时候确定?
- this 的四大核心绑定规则
- 函数独立调用,非严格模式指向全局对象(浏览器中是 window),严格模式下 this 是 undefined。
- 函数作为对象的一个方法调用时,this 指向对象。函数赋值给新变量后,this 的指向丢失。
- 强制绑定 this 的指向(call, apply, bind)。
- new 的绑定。this 指向新创建的对象。
- 创建新的空对象{}
- 新对象的原型链接到构造函数的 prototype 对象上。
- 新对象为函数调用时的 this。
- 隐式返回 this 对象,也是新对象。
- 箭头函数的 this 为何不同?
- 没有自己的 this 绑定,this 在定义时所在的词法作用域。 之后无法通过 call,apply,bind 修改。
- 如何判断 this 的指向?
一个特殊对象的引用,运行时绑定的,函数被调用时确定,由函数的 call-site 调用方式确定。
回调函数中的 this 指向丢失问题? 可以使用箭头函数解决。
call, apply, bind 的实现原理是?
eval, setTimeout, <script>标签。
javascript 异步编程 🔗
实现 MyPromise 🔗
class MyPromise {
static PENDING = "pending";
static FULFILLED = "fulfilled";
static REJECTED = "rejected";
constructor(executor) {
this.status = MyPromise.PENDING;
this.value = undefined;
this.reason = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.)
};
const reject = () => {
};
try {
executor(resolve, reject);
} catch(error) {
reject(error);
}
}
then() {
}
catch(onRejected) {
return this.then(null, onRejected);
}
static resolve(value) {
return MyPromise((resolve, _) => {
resolve(value);
});
}
static reject(reason) {
return MyPromise((_, reject) => {
reject(reason);
});
}
}
async, await 底层运行机制 🔗
优点是代码风格是流程线性,同步。 基于 Promise 和生成器 Generator 实现的。
typescript 🔗
as 类型断言(type asserttion), 强制断言为某个类型。覆盖原来的类型推断。
即使缺少属性也不会报错。
satisfies 仅约束目标类型不改变推断。
extends 类型约束(用于范型)
is 类型谓词。
infer 类型推断(用于条件类型)
类型约束的关键词 🔗
`**
核心配置文件:tsconfig.json
cocos creator 3.x 的游戏开发 🔗
B 站 Siki 老师的(Cocos 开发入门)TypeScript 零基础入门视频教程。 需要 1 天时间,快速入门就够了。
与 c++,java 这类先定义类在定义对象的做法很大不同,js 的对象定义是动态,对象的属性和方法可以随时增删改。
typescript -- tsc 编译--> app.js
node app.js
ts 是静态类型语言,在编译期做类型检查。也是弱类型语言,允许隐式类型转换。
TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性。
约定使用 TypeScript 编写的文件以 .ts 为后缀,用 TypeScript 编写 React 时,以 .tsx 为后缀。
如果要在报错的时候终止 js 文件的生成,可以在 tsconfig.json 中配置 noEmitOnError 即可。
安装 node.js 🔗
首先需要安装Node.js, 最好通过 nvm 管理 node 的版本。 nvm ls-remote 查看远程版本, nvm install 24.0.0, nvm use 24.0.0.
This package will install:
• Node.js v18.15.0 to /usr/local/bin/node
• npm v9.5.0 to /usr/local/bin/npm
修改 npm 获取包的地址: npm config set registry https://registry.npm.taobao.org
npm 安装 typescript 🔗
npm install typescript --save-dev
https://www.typescriptlang.org/docs/handbook/typescript-from-scratch.html
基本语法 🔗
静态类型, 类和模块。
迭代器,生成器,装饰器,命名空间。 https://www.typescriptlang.org/docs/handbook/intro.html
迭代器: 实现 next()方法的对象。
生成器: 返回迭代器对象的函数,主要作用是让创建迭代器对象的过程变简单。
可迭代对象具有Symbol.iterator属性。
function genIterator(items) {
let i = 0;
return {
next: function() {
var done = (i >= items.length);
var value = !done ? items[i++] : undefined;
return {
done: done
value: value
};
}
};
}
let iterator = genIterator([1,2,3]);
console.log(iterator.next());
// 生成器
function *genIter() {
yield 1;
yield 2;
yield 3;
}
let arrs = [1,2,3];
let iterator = arrs[Symbol.iterator]();
let obj = {
items: [1,2,3],
*[Symbol.iterator]() {
for (let item of this.items) {
yield item;
}
}
}
for (let num of obj) {
console.log(num);
}
类型 🔗
never 类型的使用场景。 🔗
- 永远不会返回的函数
- 完整性检查 (Exhaustive Checking)在 switch 或 if 的场景下,never 常用于 检查是否漏掉 case
- 不可能存在的类型。 type A = number & string; // 永远不可能有值 => never
- 在错误处理时,可以标记某个分支“不应该到达”
function throwError(msg: string): never {
throw new Error(msg); // 函数抛错
}
function infiniteLoop(): never {
while (true) {} //死循环, 永远不会返回的函数
}
any, unkown?
任意值(Any)用来表示允许赋值为任意类型。unknown 表示跟 any 类型一样接受任意类型的值,但对 unknown 类型的操作必须先进行类型确认, 确保类型安全。
定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查
let number;
number = "seven";
number = 7;
interface, type 的区别? interface 和 class 的区别? 类型约定,无函数实现。后者是有函数实现,可实例化。
interface 描述对象的结构。class 描述对象的模版,数据+函数。
语法层面:
运行时层面: interface 编译后 完全被擦除,在运行时 不存在。
class 在运行时存在, 会编译成一个 构造函数 + 原型方法。可以 new 出实例,实例上挂数据,原型上挂方法
infer 是在做什么? 🔗
infer 关键字只能在 extends 条件类型 里用。作用是:在条件类型中引入一个待推断的类型变量。
T extends SomeType<infer U> ? U : Fallback意思是 infer U 表示要从 SomeType<...> 里 推断出内部的类型参数,并临时起名为 U。
//获取函数返回值类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; // infer R 把函数的返回值类型提取出来。
type Fn = (a: number) => string;
type Result = ReturnType<Fn>; // string
//获取函数参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never; //infer P 把函数参数列表提取为元组类型。
type Fn = (a: number, b: string) => void;
type Params = Parameters<Fn>; // [number, string]
//获取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<number[]>; // number
//提取 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number
//多层嵌套推断
type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T; //infer 可以用于递归推断
type Arr = number[][][];
type Flat = Flatten<Arr>; // number
ts 中的类型兼容。 🔗
TypeScript 的类型兼容遵循 结构化类型系统 (structural typing),即 鸭子类型:只要结构满足,就算兼容。
- 对象类型:只要目标类型的属性,在源类型中都存在(且类型兼容),就兼容。
- 函数类型:比较 参数 和 返回值,涉及 协变 / 逆变。
- 枚举 / 基本类型:字面量类型可以兼容更宽泛的类型。
协变,逆变: 这两个概念主要出现在函数类型兼容性的比较中。
返回值类型是协变的意思是:子类可以赋值给父类。返回值可以“更具体”。
class Animal {}
class Dog extends Animal {}
type Fn1 = () => Animal;
type Fn2 = () => Dog;
let f1: Fn1;
let f2: Fn2; //子类
// 子类赋值给父类。
f1 = f2; // ✅ Dog 是 Animal 的子类(协变)
参数类型是 逆变的意思是:父类参数可以赋值给子类参数。参数可以“更宽泛”。
type HandlerA = (dog: Dog) => void;
type HandlerB = (animal: Animal) => void;
let hb: HandlerB; // 父类
let ha: HandlerA; // 子类
// 父类赋值给子类
ha = //??; // ✅ Animal 参数更宽松,可以接 Dog
总结:
- 返回值(协变):子类返回值兼容父类。
- 参数(逆变):父类参数兼容子类参数
联合类型使用 | 分隔每个类型
接口、类、对象 🔗
使用接口(Interfaces)来定义对象的类型。
接口=状态字段+行为方法的签名
类=状态字段 + 实现方法
对象是包含一组键值对的实例
声明文件 🔗
Express web server 🔗
npm install -g express && npm install -g express-generator
react 🔗
https://react.dev/learn/start-a-new-react-project
组件 🔗
HTML 标签必须全部为小写字母, React 组件标签必须为大写字母开头。
快速搭建 react 项目 🔗
npm config set registry http://registry.npm.taobao.org/ npx create-react-app react-app 这一步很耗时。 cd react-app && npm start
将 ts 和 react 结合起来使用 🔗
npm install -g create-react-app create-react-app my-app --scripts-version=react-scripts-ts
tsconfig.jsonTypeScript 特定的选项。package.json依赖。
src 包含了 TypeScript 和 CSS 源码。index.tsx 是强制使用的入口文件。
现成的项目 🔗
Geeker-admin
node.js 🔗
《深入浅出 node.js》
调试 node 程序。在代码中加入 debugger
web 服务器的要点: 事件驱动,非阻塞 io。
模块机制 🔗
node.js v12 开始支持 es module。
common.js模块类型: 内置,自定义文件,第三方模块。
模块作用域。
(function (exports, require, module, __filename, __dirname) {
// 模块代码。
});
ES module 🔗
<script type="module">
// 以ES module的标准执行其中的js代码
// ESM自动采用严格模式, 例如不能使用全局this
// 每个ES module都是私有作用域
// ESM是通过CORS的方式去请求外部js模块
// ESM的script标签会延迟执行脚本,等同于defer属性。
</script>
把 js 代码和 js 运行时打包为一个可执行文件 🔗
node 的方式 🔗
deno 的方式 🔗
bun 的方式 🔗
css 🔗
围绕着 html 元素的选择,样式,布局展开的系统。
描述 html 中标签的样式格式。
语法规则: 声明语句,选择器。
css 的引入方式 🔗
内联样式。<p style="color:red;font-size:20px">内联css</p>。
内部样式。例如单个文档需要使用特殊样式时。
<head>
<style>
p {
color: red;
font-size: 20px;
}
</style>
</head>
外部样式。例如样式需要应用到多个文档时。
<link ref="stylesheet" type="text/css" href="abc.css"></link>
选择器 🔗
通用选择器,
标签选择器,符合选择器, 后代(div p), 直接子元素(div > p)。
类选择器(.className),
id 选择器(#idName)
选择器有哪些?(元素、类、ID、属性、伪类、伪元素…)
优先级规则:!important > 内联 > ID > 类/属性/伪类 > 标签/伪元素 > 继承。
盒子模型 🔗
外边距 margin, 边宽 border,内边距 padding, heigth, width, 内容 element
层叠,优先级,继承 🔗
- 组成层叠的四个部分
- 层叠和继承的区别
- 如何控制样式和元素的对应关系
- 简写声明的常见误区
层叠:如何解决同一个 web 元素上的 css 样式冲突问题。 样式表的来源优先级(外部文件,内联代码,行内样式),选择器的优先级,代码顺序。 例如 css 的选择器有标签选择器,ID 选择器,类选择器, 选择器的优先级。
继承: 部分属性,如 color, font-size 会从父元素继承,有的不会继承,如 border, margin 等。
相对单位 em, rem, vw,vh
css 布局 🔗
浏览器默认的 html 布局方式是:正常布局流 normal flow。元素默认的排列方式: 块级元素垂直排列,行内元素水平排列。其他布局都是对正常布局流的打破。
下面的布局技术会覆盖默认的布局行为:
- display
- 浮动 float
- position 属性
- 表格布局
- flexbox 弹性盒子。一维布局,行或者列。
block 元素:div, p, h1, ul等。 inline 元素:span, a, strong。
出现在一个元素下面的元素为块元素,出现在一个元素旁边的是内联元素。
层叠层,z 轴方向的层叠优先级 z-index 🔗
css 布局只网页中的标签在 x 轴水平,y 轴垂直上的布局。css 中是三维渲染的,也有 z 轴上的堆叠规则。所以引出层叠层, z-index概念。
z-index 只有在层叠层中才有意义。position: static 的元素,不会生成新的层叠上下文,也就没有“层叠层级”的概念。z-index 就算写了也 不生效。
只有定位元素(relative / absolute / fixed / sticky),才会让浏览器把它单独抽出来,放进“可层叠比较”。z-index 在 flex 子项 和 grid 子项 上(即使 position: static)也能生效,因为这些布局上下文会为子项建立 层叠上下文。
所以核心明确两点: 哪些属性会创建层叠上下文? 层叠顺序是什么?
哪些属性会创建层叠上下文?
- 定位元素, position 不为 static, z-index 不为 auto
- flex,grid 子元素
- 特殊属性
- opacity, transform, filter 滤镜,isolation 等
z-index 只能再同一个层叠上下文内才能比较。
层叠顺序(stacking order): 先背景 / 边框, 再普通文档流块级元素,再浮动元素,再定位元素(z-index: auto), 再定位元素(z-index: 数值,按数值大小)。
浮动布局
float 属性。 display 属性。
Flexbox
grid
position 属性
响应式设计
父元素高度塌陷: 子元素设置浮动后会脱离正常文档流,父元素计算高度时会忽略浮动的子元素,导致父元素高度无法被撑开。
BFC, 是一个独立的空间,空间里的元素布局不会影响外部,外部也不影响内部。
BFC 的特点:
- 容器内的浮动元素会被计算入容器高度。解决高度塌陷。
- 容器内元素的 margin 不会于外部元素的 margin 合并。
- 容器不会与浮动元素重叠。
触发父元素成为 BFC 的方式:overflow: hidden, display: flow-root。
使用 tailwind css 🔗
我暂时放弃使用 assistant-ui 了,太难用了。 在 html 的右下角有一个 ghost icon 的 button,点击后显示 modal,里面可以 ai chat。
安装 tailwind css 🔗
npm install -D tailwindcss postcss autoprefixer可以下载安装成功,但是在执行
npx tailwindcss init -p 时报错 npm ERR! could not determine executable to run。使用了错误的版本(如 v4.1.16), 还在 beta / pre-release 阶段,有兼容问题。
使用npm install -D tailwindcss@3 postcss autoprefixer 安装稳定版 TailwindCSS 3.x。
初始化 Tailwind 配置 🔗
npx tailwindcss init -p会生成tailwind.config.js和postcss.config.js。
tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: { extend: {} },
plugins: [],
};
创建 index.css 文件,并 import 到入口 js 文件中 🔗
index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
问题,className 会很长。 🔗
前端工程化 🔗
为什么需要工程化? 🔗
所有 JS 都直接用 <script> 标签引入导致了
| 问题 | 说明 |
|---|---|
| 命名冲突 | 所有变量、函数都在全局作用域,function init() 一不小心就重名了 |
| 依赖顺序难维护 | b.js 依赖 a.js,但加载顺序没搞对就报错 |
| 文件数量爆炸 | 稍大一点的项目可能有几十上百个 JS 文件,每个都要 <script> 引入 |
| 重复逻辑多、难复用 | 没有模块系统,无法拆分和复用逻辑 |
| 多人协作困难 | 不同人改不同文件,全局污染、冲突频发 |
| 上线性能差 | 浏览器一次性要加载几十个小文件,请求开销大 |
| 没有自动构建 | 想压缩、混淆、加 hash,都要人工处理 |
| 无法使用新特性 | 浏览器不支持 ES6、TypeScript、SCSS 等语法 |
工程化的方向 🔗
| 方向 | 工具/手段 | 作用 |
|---|---|---|
| 模块化 | ES Modules / CommonJS / Webpack | 拆分 JS 文件、按需加载 |
| 组件化 | React / Vue / Svelte | 拆分 UI 结构,复用、组合 |
| 自动化构建 | Webpack / Vite / Rollup | 打包、压缩、转译、优化 |
| 自动化测试 | Jest / Cypress | 保证功能正确 |
| 自动部署 | CI/CD、Docker | 从提交代码到上线自动化 |
| 代码质量控制 | ESLint / Prettier | 统一规范、减少 bug |
常见考点 🔗
水平垂直居中的写法 🔗
Flex 布局。简单可靠。
.parent {
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
height: 300px;
}
Grid 布局。最短代码。
.parent {
display: grid;
place-items: center; /* 水平 + 垂直居中 align-items + justify-items的缩写 */
height: 300px;
}
绝对定位 + transform。兼容性好,经典写法。
.parent {
position: relative;
height: 300px;
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
三栏布局,圣杯布局,flex, grid 🔗
自适应宽高比的盒子,padding 百分比 🔗
清除浮动的方法 🔗
clear: both
伪元素 .clearfix::after { content:""; display:block; clear:both; }
BFC overflow: hidden
display: flow-root(现代方案)
模块化组织 css 🔗
模式库 🔗
————