前端

· 9969 words · 20 minute read

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.json TypeScript 特定的选项。 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: hiddendisplay: 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.jspostcss.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 🔗

模式库 🔗

————

阴影,渐变,混合模式 🔗

对比,颜色,间距 🔗

网页动效 🔗

变换 🔗

关键帧动画 🔗


所有的 css 选择器类型 🔗

预处理器 🔗