前端知识体系之 JS 指南

112 阅读35分钟

JavaScript 指南

本文档用于梳理 JS 的重难点知识,用于复习巩固, 也用于面试谈经。

JS 入门

官方文档:developer.mozilla.org/zh-CN/docs/…

JS 语法

1 数据类型

数据的类型分为以下八种:

  • 基本类型:

    • null:无效的值
    • undefined:变量未赋值时的状态
    • boolean:布尔值
    • number:整数或浮点数
    • bigint:任意精度格式的整数
    • string:字符串
    • symbol:唯一且不可改变的数据类型
  • 对象类型:object

2 基本类型

基本类型本身没有属性,但是却可以访问属性。因为在访问基本类型的属性时,会创建一个对应的包装类对象,然后访问包装对象的属性。比如:访问字符串的属性时,会自动创建一个 String 对象。

基本类型在计算时,会进行类型转:

  • 字符串和数字做加法运算时,会把数字转换为字符串;做减法运算时,会把字符串转换为数字。
  • null 值,在数值类型环境中会被当作 0 ;在布尔类型环境中会被当作 false。
  • undefined 值,在数值类型环境中会被转换为 NaN;在布尔类型环境中会被当作 false。

3 对象类型

3.1 定义

对象类型(object 类型)是一种包含属性对的复杂数据类型,分为以下三种:

  • Object:对象类型的超类(也叫原型)。
  • 内部类:JS 内部的 Object 子类,比如:Date。
  • 自定义类:自定义的 Object 子类。

对象类型本身也是一个构造函数,可以通过 new 创建对象实例(也叫对象),比如:

let a = new Object('tom'); //String {'tom'}
let b = new String(1); // String {'1'}

对象类型如果是通过 function 声明的,那它也是一个普通函数,可以直接调用,比如:

let a = Object('tom'); //String {'tom'}
let b = String('1'); //'1'

对象类型(包括 Object 和 Function)本身也是一个 Function 对象,比如:

function Demo() {}
​
console.log(Object.constructor === Function); // true
console.log(Function.constructor === Function); // true
console.log(Demo.constructor === Function); // true

3.2 属性

对象类型作为一个 Function 对象,包含以下属性:

  • name:对象类型的名称

  • length:对象类型的长度,一般为 1。

  • prototype:对象类型的原型数据,是可以被子类继承的数据,包含以下属性:

    • constructor:对象类型自身的构造函数数据。
    • [[Prototype]]:指向父类(一般是 Object)的 prototype。
    • 原型属性:直接定义在 prototype 中的属性。
  • [[Prototype]]:指向 Function 的 prototype。

  • 静态属性:直接定义在对象类型中的属性。

[[Prototype]] 属性不能直接访问,可以通过 Object.getPrototypeOf() 访问,也可以通过 __proto__ 属性访问,比如:

let objectProto = Object.getPrototypeOf(Object);
console.log(objectProto === Function.prototype); //true。let functionProto = Function.__proto__;
console.log(functionProto === Function.prototype); //true。

如果想查看对象类型的父类,可以通过 prototype.__proto__.constructor 来判断,比如:

let objectParent = Object.prototype.__proto__;
console.log(objectParent === null); //true。Object 是所有对象类型的超类
​
let functionParent = Function.prototype.__proto__;
console.log(functionParent.constructor === Object); //true。Function 的父类为 Object

3.3 对象类型与基本类型

对象类型的特点:

  • 在堆中保存值,在栈中保存值的引用地址,变量等于值的引用地址
  • 值是可变的,可以扩展属性和方法
  • 对象类型的相等性比较的是值的引用地址。详情见:相等性判断

基本类型的特点:

  • 在栈中保存值,变量等于值
  • 值是不可变(不能改变自身,只能直接替换)的,不可以扩展属性和方法
  • 基本类型的相等性比较的是值。详情见:相等性判断

4 变量声明

变量声明有三种方式:

  • 第一种是 var,特点如下:

    • 作用域是函数。在函数内声明的是局部变量;在函数外(JS 的顶层)声明的是全局变量(全局对象的属性)。
    • 支持变量提升:var 声明的变量,会先在代码块的顶部声明这个变量,并且赋予 undefined 的值;当变量实际声明时,才会赋予实际的值。
    • 在同一作用域中,var 声明的变量名能够多次声明。
  • 第二种是 let,特点如下:

    • 作用域是语句块。在语句块内声明的是局部变量;在语句块外(JS 的顶层)声明的也是局部变量,但是作用域是全局。
    • 支持变量提升:let 声明的变量,会先在代码块的顶部声明这个变量,但是不赋予初始值;当变量实际声明时,才会赋予实际的值。在实际声明之前访问变量,会抛出引用错误(ReferenceError),因为变量处在一个"暂时性死区"。
    • 在同一作用域中,let 声明的变量名不能与其他变量、常量、函数的名称相同。
  • 第三种是直接声明,跟 var 类似,但是在严格模式'use strict'; )下会报错。

常量声明使用 const,特点如下:

  • 初始化必须赋值
  • 作用域是语句块。在语句块外声明的也是局部常量,但是作用域是全局。
  • 支持常量提升。同 let。
  • 在同一作用域中,声明的常量名不能与其他变量、常量、函数的名称相同。

5 布尔值

布尔类型有两种字面量:true 和 false。

5.1 布尔环境

在布尔环境中,null、undefined、0、-0、NaN、空字符串("")会当做 false 处理;其他值会当做 true 处理,比如:

if (!0) console.log('0 is false'); // 0 is false
if ([]) console.log('[] is true'); // [] is true

5.2 Boolean 对象

Boolean 对象是一个布尔值的对象包装器,如果参数值为缺省值、false、null、undefined、0、-0、NaN、空字符串(""),那么该对象的初始值为 false;否则,初始值为 true。

Boolean 对象不是布尔类型,不能直接进行条件判断,比如:

const x = new Boolean(false);
if (x) {
  // 这里的代码会被执行
}

如果希望将数据转为为布尔类型,有两种方法:Boolean()、!!

const x = Boolean(expression);     
const x = !!(expression);          

6 数字

数字包括多种基数的整数字面量和以 10 为基数的浮点数字面量,还包括三种符号值:+ Infinity(正无穷)、- Infinity(负无穷)和 NaN(not-a-number,非数字)。

6.1 整数字面量

整数可以用以下方式表示:

  • 二进制整数以 0b(或 0B)开头,只能包含数字 0 和 1。
  • 八进制的整数以 0(或 0o、0O)开头,只能包括数字 0-7。
  • 十进制整数字面量由一串数字序列组成,且没有前缀 0。
  • 十六进制整数以 0x(或 0X)开头,可以包含数字(0-9)和字母 af 或 AF。

严格模式下,八进制整数字面量必须以 0o 或 0O 开头,而不能以 0 开头。

6.2 常见方法

Number 的 toString() 可以将数字转换成多种进制的字符串,比如:

let a = 1212;
console.log(a.toString()); //1212。默认为十进制。
console.log(a.toString(16)); //4bc。当前为十六进制

Number 的 toFixed() 方法可把 Number 四舍五入为指定小数位数的数字。比如:

console.log(1.35.toFixed(1)); // 1.4

6.3 内存表示

在代码中,数字字面量是无符号的,如果想表示负数,需要使用一元操作符 -

在内存中,数字均为 IEEE 754 64 位浮点类型,包含以下部分:

  • 符号位(sign,S ):1 位,0 - 正数,1 - 负数
  • 指数位(mantissa,M):11 位,表示 1 ~ 2046(0 和 2047 是特殊值) 的指数
  • 尾数位(exponent,E):52 位,表示小数点后的二进制数

尾数表示实际值的有效数值部分,指数是尾数应乘以的 2 的幂。将其视为科学计数法:Number = (−1) ^ S × 1.E × 2 ^ (M - 1023),取值范围为 - 2 ^ 1024 + 1 和 2 ^ 1024 - 1

备注:小数部分转二进制的方法如下:对小数部分 × 2,小数点前为几,则二进制中记几;重复上述操作,直到乘位为 0 。比如:0.75 转为二进制为 .11。

6.4 属性访问

数字可以访问包装类的属性,但是不能直接访问,因为直接访问(.)会当做数字处理,需要使用 .. 或者变量的方式,比如:

1.toFixed();  // 1
1..toFixed(); // VM250:1 Uncaught SyntaxError: Invalid or unexpected token
let a = 1;
a.toFixed();  // 1

7 bigint

bigint 可以精确表示超过安全整数范围(- 2 ^ 53 + 1 到 2 ^ 53 - 1 之间的整数,可以通过 Number.MIN_SAFE_INTEGER 和 Number.MAX_SAFE_INTEGER 获取)的整数。

bigint 与 number 有以下区别:

  • bigint 不能用于 Math 对象中的方法;
  • bigint 不能和任何 number 值混合运算,两者必须转换成同一种类型。在两种类型来回转换时要小心,因为 bigInt 在转换成 number 时可能会丢失精度。

8 字符串

8.1 字符串转数字

字符串转数字有四种方法:

第一种:parseInt(),将多种进制的字符串转换为整数,如果字符串没有进制标识符,那么参数中需要带上进制标识,比如:

console.log(parseInt('0xaaa')); //2730
console.log(parseInt('aaa', 16)); //2730

第二种:parseFloat(),将十进制的字符串转换为数字

第三种:Number(),将多种进制的字符串转换为数字

第四种:一元加法运算符,将多种进制的字符串转换为数字,比如:

console.log("1.1" + "1.1"); // '1.11.1'
console.log((+"1.1") + (+"1.1")); // 9.2

8.2 字符串补全

padStart 与 padEnd 可以对字符串进行补全:

let a = '123';
console.log(a.padStart(5, '0')); //00123
console.log(a.padEnd(5, '0'));   //12300

9 函数

函数是一个可以直接调用的子程序,也是一个自定义的 Object 子类。

9.1 函数定义

定义一个函数有以下方法:

第一种是函数声明:

function name([param[, param[, ... param]]]) { statements }

注意: 在 JS 的顶层声明的函数属于全局变量,可以通过全局对象访问。

第二种是函数表达式:

let myFunction = function name([param[, param[, ... param]]]) { statements }

注意:

  • 函数表达式可以省略函数名,也就是匿名函数。但是不建议省略,因为遇到错误时,堆栈跟踪会显示函数名,容易寻找错误。
  • 当函数只使用一次时,通常使用 IIFE (立即调用的函数表达式)。 IIFE 还用于防止污染全局变量。

第三种是 Function 构造函数表达式,一般用于动态创建函数,语法如下:

let myFunction = new Function (arg1, arg2, ... argN, functionBody)

下面是一个简单的示例:

let log = new Function('name', 'age', 'console.log(name)');
log('张三', 2); // 张三

注意:

  • 构造函数表达式会遇到和 eval() 类似的的安全问题和(相对较小的)性能问题。

第四种是箭头函数表达式,是一种简洁的函数声明,语法如下:

let myFunction = ([param] [, param]) => { statements } param => expression

注意:

  • 箭头函数不是构造函数,不能进行 new 操作。
  • 箭头函数没有自己的thisargumentssupernew.target,会从自己的作用域的上一层继承 this。
  • 箭头函数的 call()、apply(),只能传递参数,不能指定 this。

第五种是函数生成器,用于控制函数的执行过程,语法如下:

function* name([param[, param[, ...param]]]) { statements }

下面是一个简单的示例:

function* generator(i) {
  yield i;
  yield i + 10;
}
​
const gen = generator(10);
console.log(gen.next().value); // 10
console.log(gen.next().value); // 20

注意: 函数生成器生成的函数不是构造函数,不能进行 new 操作。

第六种是生成器构造函数表达式,跟函数生成器的功能一致,语法如下:

let myFunction = new GeneratorFunction (arg1, arg2, ... argN, functionBody)

注意: 生成器构造函数表达式生成的函数不是构造函数,不能进行 new 操作。

9.2 函数参数

声明函数时,可以指定参数,有四种方式:

第一种:直接声明,比如:

function log(num1, num2 = 3) {
  let res = num1 * num2;
  console.log('res', res);
  return res;
}
log(10); // 30

注意: 在声明参数时,可以指定默认值。默认值只有在不传值,或者传 undefined 的情况下才会生效。

第二种:解构赋值,从对象中获取同名的属性赋值给参数,比如:

function log({name}) {
  console.log('res', name);
}
log({name: '张三'}); // 张三

注意: 在使用解构赋值时,也可以指定默认值。

第三种:剩余参数,将不确定数量的参数表示为数组,比如:

function log(...list) {
  console.log('res', list[0]);
}
log(1, 2, 3, 4); // 1

第四种:直接使用,不声明参数,通过 arguments 对象来使用,比如:

function log() {
  console.log('res', arguments[0]);
}
log(1, 2, 3, 4); // 1

注意:

  • arguments 对象不能在箭头函数中使用。
  • 剩余参数只包含那些没有对应形参的实参; arguments 对象包含了传给函数的所有实参。
  • 剩余参数是数组;arguments 对象只是类数组。

9.3 函数属性

函数是自定义类,也包含属性,分为三种:

第一种,实例属性,是定义在对象实例中的属性,只能通过对象实例访问,比如:

function Demo(age) {
  this.age = age;
}
​
let data = new Demo(18);
data.name = '张三';
console.log(data.age); // 18

注意: 在创建实例后,还可以添加实例属性。

第二种,原型属性,是定义在函数的 prototype 中的属性,可以被子类继承,可以被对象实例直接访问,比如:

function Demo() {}
​
Demo.prototype.age = 18;
let data = new Demo();
console.log(data.age); // 18

注意: 所有的对象实例共享一个 prototype。

第三种,静态属性,是定义在函数自身中的属性,只能被函数直接访问,比如:

function Demo() {}
Demo.age = 18;
console.log(Demo.age); // 18

9.4 函数继承

如果想让函数继承其他类型,可以通过 Object.setPrototypeOf(),分为以下两种:

第一种,继承 prototype :只继承原型属性,比如:

function Demo() {
  this.name = 'tom';
}
​
function Demo2() {}
​
Demo.prototype.age = 18;
Object.setPrototypeOf(Demo9.prototype, Demo.prototype);
let data = new Demo2();
console.log(data.name); // undefined
console.log(data.age); // 18

第二种,继承对象:继承实例属性和原型属性,比如:

function Demo() {
  this.name = 'tom';
}
​
function Demo2() {}
​
Demo.prototype.age = 18;
Object.setPrototypeOf(Demo9.prototype, new Demo());
let data = new Demo2();
console.log(data.name); // tom
console.log(data.age); // 18

9.5 函数提升

函数提升就是将函数声明的代码提升到代码块的顶部。函数声明会进行函数提升,函数表达式不会进行函数提升。

test1(); //正常运行
function test1() {}
​
test2(); //Uncaught TypeError: test2 is not a function。此时 test2 为 undefined。
var test2 = function () {};

9.6 函数作用域

函数作用域就是函数的访问范围,分为两种情况:默认情况和严格模式。

默认情况下:

  • 函数的作用域是函数内外;
  • 在函数内声明的是局部变量;在函数外(JS 的顶层)声明的是全局变量。

严格模式下:

  • 函数的作用域是语句块内外。
  • 在语句块内声明的是局部变量;在语句块外(JS 的顶层)声明的是全局变量。

注意:使用表达式声明的函数,作用域等同于变量的作用域。

9.7 闭包

函数支持嵌套,嵌套函数(也叫内部函数)可以直接访问外部函数能够访问的所有数据(变量或函数),外部函数不能直接访问内部函数中定义的变量和函数。

闭包就是在函数内部像内部数据一样访问外部数据。

闭包的原理:

  • 声明函数时,如果内部访问了一个数据,就从内到外一层一层找这个数据,如果这个数据是外部数据,就会保存这个数据的引用;
  • 调用函数时,不是从内到外一层一层找访问的数据,而是直接从保存的引用中找。

闭包的利弊:

  • 好处是方便和安全:不用传参就可以使用外部数据;在其他地方也能正确使用外部数据(即使函数调用的地方有同名数据)。
  • 坏处是容易造成内存泄露。

9.8 函数声明 vs 函数表达式 vs 构造函数表达式

函数声明、函数表达式、构造函数表达式的作用差不多,但是还有一些差别:

第一点,函数表达式中的函数名只能在函数内部使用;构造函数表达式定义的函数没有函数名。

第二点,函数声明支持函数提升;函数表达式和构造函数表达式定义的函数不支持。

第三点,函数声明和函数表达式支持闭包;构造函数表达式不支持。

第四点,通过函数表达式和函数声明定义的函数只会被解析一次;通过构造函数表达式定义的函数会被解析多次,每次调用这个函数的构造函数时,都会解析一次 functionBody(字符串函数体)

注意: 在解析 Function 构造函数表达式的字符串函数体时,内嵌的函数表达式和函数声明不会被重复解析。

9.9 方法函数

方法函数,也叫方法,是对象或类的函数类型的属性。

从 ES6 开始,在对象中定义方法有了更简洁的写法,比如:

const obj = {
  foo() {
    return 'bar';
  }
};
​
console.log(obj.foo());

注意:

  • 方法函数不是构造函数,不能 new。
  • 生成器方法、Async 方法、Async 生成器方法都支持这种简写。
  • 该简写方法还支持计算属性名(使用变量表示的属性名),比如:var bar = {['foo' + 2]() { return 2; }};

9.10 访问器字段

访问器字段就是 getter 和 setter,用于定义属性的读写操作。

9.10.1 getter

getter 是一个特殊的方法函数,能够定义对象或类的属性的读操作,语法如下:

{ get prop() { ... } }

其中,prop 就是对象或类的属性名,支持计算属性名。

下面是一个简单的示例:

const obj = {
  log: ['a', 'b', 'c'],
  get latest() {
    return this.log[this.log.length - 1];
  }
};
​
console.log(obj.latest); // c

getter 相当于定义了一个伪属性,一般用于返回动态计算值。

9.10.2 setter

setter 是一个特殊的方法函数,用于定义对象或类的属性的写操作,语法如下:

{ set prop() { ... } }

其中,prop 就是对象或类的属性名,支持计算属性名。

下面是一个简单的示例:

let language = {
  log: [],
  set current(name) {
    this.log.push(name);
  }
};language.current = 'EN';
language.current = 'FA';
console.log(language.log); // ['EN', 'FA']

setter 相当于定义了一个伪属性,一般用于监听属性。

注意: 不能在 setter 中直接给 getter 属性赋值。

9.10.3 Object.defineProperty()

通过 Object.defineProperty() ,可以给一个现有对象添加 getter、setter,比如:

let data = {name: '张三', age: 18};
​
Object.defineProperty(data, 'name1', {
  get: function () {
    return this.name + ',该下班了!';
  }
});
console.log(data.name1); // 张三,该下班了!
​
Object.defineProperty(data, 'age1', {
  set: function (x) {
    this.age = x + 10;
  }
});
data.age1 = 10;
console.log(data.age); // 20

9.11 原生方法

Function.prototype.call() 方法使用一个指定 this 值和单独给出的一个或多个参数来调用一个函数。实现方法如下:

// obj 是需要绑定的 this 值
Function.prototype.call = function (obj, ...args) {
  obj = obj == null || obj == undefined ? globalThis : Object(obj); //将 obj 转为为对象类型,其中 globalThis 指全局对象let key = Symbol('temp'); //唯一的 key
  obj[key] = this; //需要绑定的方法
  let res = obj[key](...args); //通过对象调用方法
  delete obj[key];
  return res;
};

注意:Function.prototype.call() 与 Function.prototype.apply() 作用相似,call() 是多个参数,apply() 是一个参数(数组或类数组对象)。

Function.prototype.bind() 方法创建一个绑定 this 值和初始参数的新函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。实现方法如下:

// context 是需要绑定的 this 值
Function.prototype.bind = function (context) {
  let fn = this; // 此处的 this 是指需要绑定的方法
  let args = Array.prototype.slice.call(arguments, 1); // 需要绑定的初始参数
  return function () {
    let args1 = Array.prototype.slice.call(arguments); // 新函数的参数
    let args2 = args.concat(args1);
    return fn.apply(context, args2);
  };
};

10 类

类是自定义的 Object 子类,跟函数相比有以下区别:

  • 类不能直接调用;函数可以。
  • 类可以封装私有字段;函数不可以。
  • 类面向对象,更加规范;函数更加灵活。

10.1 类的定义

创建类有两种方式:

第一种,使用类声明创建类:

class Rectangle {
  // 构造函数
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}
let data = new Rectangle(100, 150);

注意: 类声明不会进行提升;函数声明会进行提升。

第二种,使用类表达式创建类:

let Rectangle = class Rectangle2 {
  // 构造函数
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
};
console.log(Rectangle.name); // 输出:"Rectangle2"

注意: 类表达式可以省略 class 后面的名称,此时类的名字就是变量的名字。

10.2 实例属性

类的数据(非函数属性)属于实例属性,比如:

class Demo {
  height = 100; //实例属性
  constructor(height) {
    this.height = height;
  }
}
​
let data1 = new Demo(200);
let data2 = new Demo(300);
console.log(data1.height); // 200
console.log(data9.height); // 300

10.3 原型属性

类的方法(函数属性)属于原型属性,比如:

class Demo {
  //原型属性
  setHeight(height) {
    this.height = height;
  }
}
​
let data = new Demo();
console.log(data.setHeight === Demo.prototype.setHeight); // true

10.4 静态属性

类的静态属性跟函数的静态属性一样,只是定义的方式不一样,比如:

静态属性就是类能够直接访问的属性,需要通过 static 关键字声明,比如:

class Demo {
  static height = 100;
  static setHeight(height) {
    this.height = height;
  }
}
​
Demo.setHeight(200);
console.log(Demo.height); // 200

静态属性还可以在静态初始块(首次加载类时运行的代码块)中设置,比如:

class Demo {
  static {
    Demo.height = 100;
  }
}
​
console.log(Demo.height); // 100

静态属性还可以跟函数一样定义,比如:

class Demo {}
​
Demo.height = 100;
console.log(Demo.height); // 100

注意:

  • 静态属性不能被对象实例访问。
  • 静态方法中的 this 指向类自身。注意: 类自身就是一个 Function 对象。

10.5 私有属性

在类中可以通过 # 定义私有属性,定义的私有属性只能在类中使用,不能被对象实例访问,比如:

class Demo {
  #color = 'red';
  getColor() {
    return this.#color;
  }
  #setColor(color) {
    this.#color = color;
  }
}
​
let data = new Demo();
console.log(data['#color']); // undefined
console.log(data.getColor()); // red
data['#setColor'](); //报错

类的私有数据属性属于实例属性,只是无法被访问;私有方法属性保存在原型的 [[PrivateMethods]] 中。

10.6 类的继承

类可以通过 extends 关键字继承其他类或函数的公共数据和方法,比如:

class Demo1 {
  color = 'red';
  setColor(color) {
    this.color = color;
  }
}
class Demo2 extends Demo1 {}
​
let data = new Demo2();
console.log(data.color); // red
data.setColor('green');
console.log(data.color); // green

类还可以像函数一样通过 Object.setPrototypeOf() 继承 prototype 或对象,详见:函数继承

在子类中,如果想调用父类的方法,可以 super 关键字。

10.7 访问器字段

访问器字段就是 getter 与 setter,与对象中的使用方法类似。详情见函数 - 访问器字段

11 对象

对象是对象类型的实例,是一系列属性的集合。

11.1 对象创建

创建对象有三种方式:

第一种是对象初始化器:通过字面量的方式创建对象,比如:

let data = {name: '张三'};

注意

  • 对象属性名如果不是合法的标识符,它必须用 "" 包裹,并且通过 [] 访问与赋值
  • 对象属性名在内存中只有两种类型:string、symbol。

第二种是Object.create:创建一个 [[Prototype]] 属性指向对象字面量的新对象。示例:

let Animal = {
  type: 'animal',
  displayType() {
    console.log(this.type);
  }
};
​
let obj = Object.create(Animal);

注意: Object.setPrototypeOf() 可以在对象创建后再设置 [[Prototype]] 属性。

第三种是构造函数:通过 new 构造函数的方式创建对象,比如:

function Demo(age) {
  this.age = age;
}
let data = new Demo(18);

在 new 构造函数的过程中,会做如下操作:

  1. 创建一个空对象:{}。
  2. 给新对象添加 [[Prototype]] 属性:指向对象类型(也叫构造函数)的 prototype;
  3. 将函数的 this 指向新对象,然后运行函数
  4. 返回函数的 this。

11.2 对象属性

对象中包含以下属性:

  • 实例属性:在对象实例中定义的属性。
  • [[Prototype]] 属性:对象的原型链上的属性。
11.2.1 实例属性

实例属性是在对象实例中定义的属性,具体可以分为以下三种:

  • 在对象字面量中定义的属性
  • 在类中定义的非静态数据
  • 在函数的 this 中定义的属性

实例属性的属性名是字符串或可以转换为字符串的其他类型,属性值可以是任何类型。

11.2.2 原型链属性

原型链[[Prototype]] )属性是对象的原型链上的属性。

其中,对象的原型链是通过 [[Prototype]] 属性连接起来的对象类型及其超类的 prototype。具体来说,[[Prototype]] 属性指向对象类型的 prototype,对象类型的 prototype 的[[Prototype]] 属性又指向父类(也叫原型)的 prototype ,这样一层一层链接,直到最后一层(一般是 Object 的 prototype)。

对象在访问属性时,先从实例属性查找,再从原型链上一层一层查找,直到找到为止。

注意: 在原型链上查找属性比较耗时,如果只是检查对象是否有某个属性,那么推荐 hasOwnProperty 方法,因为它不会遍历原型链。

11.2.3 属性遍历

如果想遍历对象的属性,有三种方法:

第一种,for in 语句,用于获取对象及其原型链中所有可枚举的属性。其中,枚举属性是指可以被遍历的属性。

第二种,Object.keys(),用于获取对象自身的所有可枚举的属性

第三种,Object.getOwnPropertyNames(),用于获取对象自身的所有属性(包括不可枚举的)

11.2.4 属性删除

如果想删除对象的实例属性,可以使用 delete 关键字,比如:

let obj = {name: '张三'};
delete obj.name;
console.log(obj); // {}

delete 还可以删除一个直接赋值的变量,比如:

a = 9;
delete a;
console.log(a); // 报错: a is not defined

11.3 类型判断

判断对象的类型有以下方法:

  • 通过 typeof 运算符判断是否对象类型或函数类型
  • 通过 instanceof 运算符判断具体类型及其父类
  • 通过 constructor 属性判断具体类型

11.4 this 指向

this 一般指向当前对象,但是实际的值又引用位置决定:

  • 在单独使用时,指向全局对象。
  • 在普通函数中,指向全局对象。在严格模式,指向 undefined。
  • 在方法函数中,指向对象实例。
  • 在构造函数中,指向对象实例。
  • 在静态函数中,指向函数自身。注意: 函数自身就是一个 Function 对象。
  • 在箭头函数中,指向上级作用域的 this。其中,上级作用域是指创建箭头函数的地方。
  • 在元素事件中,指向接收事件的元素。

另外, 函数的 call() 和 apply() 可以指定 this 值。

11.5 增强的对象字面量

在 ES2015,对象字面值扩展支持在创建时设置原型,简写了 foo: foo 形式的属性赋值,方法定义,支持父方法调用,以及使用表达式动态计算属性名。比如:

var obj = {
    // __proto__
    __proto__: theProtoObj,
    // Shorthand for ‘handler: handler’
    handler,
    // Methods
    toString() {
     // Super calls
     return "d " + super.toString();
    },
    // Computed (dynamic) property names
    [ 'prop_' + (() => 42)() ]: 42
};

11.6 Object 与 Map 的比较

  • Object 的键为 string、symbol 类型;Map 的键为任意类型
  • Object 的属性先按照数值大小升序排列数字类型的键,再按照插入顺序排列其他的键;Map 按照元素的插入顺序进行排列。
  • Object 的属性长度需要手动计算;Map 的元素长度可以直接获取;

11.7 对象继承

对象的继承有以下方法:

  • 原型链继承:通过 Object.setPrototypeOf() 继承父类,分为继承原型和继承实例。详情见:JS 语法 》函数 》函数继承
  • 类继承:通过 extends 继承父类。
  • 属性拷贝:将父类的属性拷贝给对象,分为浅拷贝和深拷贝。
  • 构造函数继承:在构造函数中通过 apply() 和 call() 继承父类。

构造函数继承只能继承实例属性,不能继承原型属性,示例如下:

function Parent(name) {
  this.name = name;
}
​
function Children() {
  Parent.call(this, 'sunshine'); // 继承了 Parent,同时传递了参数
  this.age = 25; // 实例属性
}
​
var instance = new Children();
console.log(instance); // { age: 25, name: "sunshine" }

12 相等性判断

比较相等性有三种方法:

  • ==:宽松相等。先转换类型,再比较值,并对 NaN、0 做特殊处理(即 NaN != NaN,+0 == -0)。
  • ===:严格相等。先比较类型,再比较值,并对 NaN、0 做特殊处理。
  • Object.is():与 === 类似,但是不对 NaN、0 做特殊处理。

12.1 ==

== 的比较规则很复杂,具体如下:

  1. 如果遇到 null、undefined,那么它们只等于本身或对方,即 null == undefined

  2. 如果类型相同,那么比较值

    1. 如果遇到 NaN、0,那么需要特殊处理:NaN != NaN,+0 == -0
    2. 如果都是对象类型,那么比较值的引用地址
  3. 如果类型不同,那么先进行类型转换,再比较值

    1. 如果其中有 symbol,那么返回 false
    2. 如果其中有 boolean,那么先将 boolean 转为 number:true = 1,false = 0,然后再次进行宽松比较
    3. 如果是 string、number、bigint 的比较,那么先转为 number,然后再比较
    4. 如果是基本类型与对象类型的比较,那么先将对象类型转为基本类型,然后再比较

对象类型转为基本类型的方法如下:

  • 如果对象有 [Symbol.toPrimitive](),就调用该方法。如果该方法返回原始值就使用,否则就抛出异常。
  • 调用 valueOf()。如果该方法返回原始值就使用,否则就调用下一步。
  • 调用 toString()。如果该方法返回原始值就使用,否则就抛出异常。

12.2 同时等于

正常情况下,一个变量只能等于一个值。如果想让一个变量同时等于多个值,那么这个变量需要是对象类型,并且重写 valueOf(),比如:

let data = {
  valueOf() {
    if (!this.value || this.value == 4) {
      this.value = 3;
    } else {
      this.value = 4;
    }
    return this.value;
  }
};
console.log(data == 3 && data == 4); // true

JS 功能

功能是内置的函数、对象。

1 Promise

1.1 异步机制

代码运行时,先同步调用 Promise 的构造函数的代码,再同步调用后续的同步代码。

如果 Promise 的构造函数中有异步代码,那么会将异步代码放入宏任务队列中,等所有的同步代码、微任务和前方宏任务执行完后再执行,当异步代码调用并执行 resolve() 后,会调用 Promise 的 then() 并将 then() 的代码放入微任务队列中,等所有的同步代码和前方微任务执行完后再执行。

如果 Promise 的构造函数中没有异步代码,那么会同步调用 Promise 的 then() 并将 then() 的代码放入微任务队列中,等所有的同步代码和前方微任务执行完后再执行。

如果 Promise 包含多个 then(),那么等前面的 then() 执行完,再调用当前的 then()。

如果 Promise 的 then() 中的代码返回一个新的 Promise,那么当 then() 执行时,会把新的 Promise 的 then() 放入微任务队列中。

下面是一个示例:

Promise.resolve()
  .then(() => {
    console.log(0);
    return Promise.resolve(4);// 将新 Primose 的 then() 加入微任务队列,当 then() 执行时会再次加入微任务队列
  })
  .then((res) => {
    console.log(res);
  });
​
Promise.resolve()
  .then(() => {
    console.log(1);
  })
  .then(() => {
    console.log(2);
  })
  .then(() => {
    console.log(3);
  })
  .then(() => {
    console.log(5);
  });
// 执行结果  0 1 2 3 4 5

1.2 代码实现

Promise 的规范:www.ituring.com.cn/article/665…

Promise 的简单实现:

class Promise {
  callbacks = [];    //回调函数
  state = 'pending'; //当前状态
  value = null;      //运行结果// 运行 _resolve、_reject
  constructor(fn) {
    fn(this._resolve.bind(this), this._reject.bind(this));
  }
​
  // 链式回调:onFulfilled, onRejected 的返回值可能为 Promise,所以 then() 统一返回 Promise
  then(onFulfilled, onRejected) {
    return new Promise((resolve, reject) => {
      this._handle({
        onFulfilled: onFulfilled || null,
        onRejected: onRejected || null,
        resolve: resolve,
        reject: reject
      });
    });
  }
​
  // 异常回调
  catch(onError) {
    return this.then(null, onError);
  }
​
  //将状态标记为完成时,并执行回调方法
  _resolve(value) {
    if (value && (typeof value === 'object' || typeof value === 'function')) {
      var then = value.then;
      if (typeof then === 'function') {
        then.call(value, this._resolve.bind(this), this._reject.bind(this));
        return;
      }
    }
​
    this.state = 'fulfilled'; //改变状态
    this.value = value; //保存结果
    this.callbacks.forEach((callback) => this._handle(callback));
  }
​
  //将状态标记为已失败,并执行回调方法
  _reject(error) {
    this.state = 'rejected';
    this.value = error;
    this.callbacks.forEach((callback) => this._handle(callback));
  }
​
  /**
   * 执行回调函数:
   * 如果状态为进行时,就保存回调函数;
   * 如果状态非进行时,就先执行 onFulfilled、onRejected 回调,再执行新 Promise 的 _resolve、_reject
   */
  _handle(callback) {
    if (this.state === 'pending') {
      this.callbacks.push(callback);
      return;
    }
​
    let cb = this.state === 'fulfilled' ? callback.onFulfilled : callback.onRejected;
    //如果 then() 中没有传递任何东西,那么直接执行新 Promise 的 _resolve、_reject
    if (!cb) {
      cb = this.state === 'fulfilled' ? callback.resolve : callback.reject;
      cb(this.value);
      return;
    }
​
    let ret;
    try {
      ret = cb(this.value);
      cb = this.state === 'fulfilled' ? callback.resolve : callback.reject;
    } catch (error) {
      ret = error;
      cb = callback.reject;
    } finally {
      cb(ret);
    }
  }
}
​
let p = new Promise((resolve) => {
  console.log('start');
  setTimeout(() => resolve('2秒'), 2000);
});
p.then((res) => console.log('then1', res)).then((res) => console.log('then2', res));

1.3 async await

async 是 Promise 的语法糖,会让函数返回一个 Promise ,比直接使用 Promise 更加简洁。

await 会等待 Promise 执行并获取其返回结果,只能在 async 函数、模块顶层中使用。await 的行为类似于 Promise.resolve(),await 的后续代码类似于 Promise.then()。

当代码运行到 await 时,被等待的表达式会立即执行,后续的代码会添加到微任务队列中异步执行。比如:

async function foo(name) {
  console.log(name, "start");
  await console.log(name, "middle");
  console.log(name, "end");
}
​
foo("First");
foo("Second");
​
// First start
// First middle
// Second start
// Second middle
// First end
// Second end// 上面的 async 函数等价于
function foo(name) {
  return new Promise((resolve) => {
    console.log(name, "start");
    resolve(console.log(name, "middle"));
  }).then(() => {
    console.log(name, "end");
  });
}

async await 虽然是 Promise 的语法糖,但是并不完全一样:

  • 异常处理。当 Promise 被拒绝时,await 会抛出异常,需要通过 try catch 处理;Promise 可以通过 then()、catch() 来处理。
  • 多个异步任务。await 无法同时运行多个异步任务;Promise 可以通过 all()、race() 同时运行多异步任务。其中,all() 在其中一个 Promise 拒绝时就会返回结果;race() 需要等所有的 Promise 处理完才会返回结果。

2 数组

循环删除数组的方法:

  • 使用数组的 filter()
  • 使用倒序 for 循环 + 数组的 splice()
  • 使用正序 for 循环 + 数组的 splice(),并在删除元素时将 i 减一,因为删除元素时,后续的元素会往前进一位。

JS API

API(Application Programming Interface,应用程序接口)是基于编程语言构建的功能模块,不需要引用(inport)就可以使用,分为两种:

  • 环境 API:JS 的运行环境(浏览器或 Node 环境)提供的 API,可以直接使用。比如:Document
  • 第三方 API:第三方平台(非运行环境)提供的 API,需要先获取才能使用。比如:新浪微博

注意:库是包含特定功能模块的一组 JS 文件,需要引用才能使用。其中,用来编写完整应用的的库叫做框架(比如:Vue)。

1 Window

1.1 网页通信

网页通信以下三种:

  • 同域通信:直接通过 window 对象进行通信,用于新页面与父页面同源的情况。
  • 跨域通信:通过 window.postMessage() 进行通信,用于新页面与父页面不同源的情况。
  • 标签页通信:通过 BroadcastChannel 进行通信,用于通用场景。
1.1.1 同域通信

如果是通过 iframe 加载子页面,那么可以用如下方式进行同域通信:

第一步,在父页面中获取子页面的 window 对象,然后进行操作,比如:

<iframe name="child" src="child.html"></iframe>
<script>
  document.querySelector('iframe').onload = function () {
    // 获取子页面的 window 对象
    const childWindow = window.frames[0];
    // 通过 window 对象操作子页面
    childWindow.document.body.style.background = 'red';
  };
</script>

注意: iframe 不能使用自闭标签。

第二步,在子页面(比如:child.html)中获取父页面的 window 对象,然后进行操作,比如:

<script>
  // 获取父页面的 window 对象
  const parentWindow = window.parent;
  // 通过 window 对象操作父页面
  parentWindow.document.body.style.background = 'gray';
</script>
1.1.2 跨域通信

如果是通过 iframe 加载子页面,那么可以用如下方式进行跨域通信:

第一步,在父页面中获取子页面的 window 对象,通过 window.postMessage() 进行通信,比如:

<iframe name="child" src="child.html"></iframe>
<script>
  //监听消息
  window.addEventListener('message', (e) => {
    console.log('收到子页面消息', e.data);
  });
  document.querySelector('iframe').onload = function () {
    // 获取子页面的 window 对象
    const childWindow = window.frames[0];
    // 通信对象的域名。如果为'*',则可以与所有域名通信。
    let origin = '*';
    childWindow.postMessage('父页面发送消息', origin);
  };
</script>

第二步,在子页面(比如:child.html)中获取父页面的 window 对象,然后进行操作,比如:

<script>
  //监听消息
  window.addEventListener('message', (e) => {
    console.log('收到父页面消息', e.data);
​
    // 获取父页面的 window 对象
    const parentWindow = window.parent;
    //通信对象的域名。如果为'*',则可以与所有域名通信。
    let origin = '*';
    parentWindow.postMessage('子页面发送消息', origin);
  });
</script>
1.1.3 标签页通信

标签页通信:developer.mozilla.org/zh-CN/docs/…

2 Event

2.1 定义

Event 接口用于描述 DOM 中的事件,比如:点击事件、键盘事件。

Event 包含以下常见属性:

  • target:触发事件的元素
  • currentTarget:事件当前指向的元素
  • bubbles:事件是否会向 DOM 上传冒泡

Event 包含以下常见方法:

  • preventDefault:取消事件的默认动作,比如:取消表单提交后默认的页面跳转动作。
  • stopPropagation:停止事件冒泡

2.2 事件流

事件流就是事件在 DOM 中的传播流程,分为两个阶段:

  • 捕获阶段:

    • 顶层元素(window 对象)传向目标元素(鼠标点击的元素)。
    • 当事件经过元素时,如果注册了捕获事件,那么会执行事件回调
  • 冒泡阶段:

    • 从目标元素传回顶层元素。
    • 当事件经过元素时,如果注册了冒泡事件,那么会执行事件回调

window.addEventListener() 默认注册的是冒泡事件,如果想注册捕获事件,那么需要第三个参数传 true 或 {capture: true}。

如果想阻止事件冒泡,那么可以在事件回调中调用 Event.stopPropagation();如果想阻止事件捕获,那么可以在事件回调中调用 Event.stopImmediatePropagation();

注意:事件流会经过 window、document 对象。

2.3 事件委托

事件委托就是在父元素上注册事件,并通过 event.target 操作子元素,比如:

<ul id="ul">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>
<script>
  let ul = document.getElementById('ul');
  ul.addEventListener('click', (event) => {
    console.log(event.target.innerHTML);
  });
</script>

事件委托的好处是只需注册一个事件,减少了事件注册的数量。

2.4 防抖与节流

防抖就是当事件频繁触发时,只执行最后一次,用于防止不小心触发的情况,代码如下:

<input /><script>
  let input = document.querySelector('input');
  input.addEventListener('keyup', debounce(task));
​
  // 实际任务
  function task() {
    console.log('向后台取数据');
  }
  // 防抖函数
  function debounce(fn) {
    let timer = null;
    return function () {
      if (timer) clearTimeout(timer);
      let work = () => {
        fn.apply(this, arguments);
        timer = null;
      };
      timer = setTimeout(work, 1000);
    };
  }
</script>

注意: 在箭头函数中,this 指向上级作用域的 this,所以 fn.apply() 中的 this 指向 debounce() 的返回函数的调用者,也就是 HTML 元素。

防抖的应用场景:自动保存、搜索建议等。

节流就是当事件频繁触发时,只在指定间隔时间后执行,防止流量浪费,代码如下:

<div draggable="true">拖拽元素</div><script>
  let div = document.querySelector('div[draggable=true]');
  div.addEventListener('drag', debounce(task));
​
  // 实际任务
  function task(e) {
    console.log(e.clientX);
  }
  // 防抖函数
  function debounce(fn) {
    let timer = null;
    return function () {
      if (timer) return;
      let work = () => {
        fn.apply(this, arguments);
        timer = null;
      };
      timer = setTimeout(work, 100);
    };
  }
</script>

节流的应用场景:滚动事件、窗口大小调整等。

3 Observer

Observer 用于监听元素的变化,包含以下几种:

  • ResizeObserver:监听目标元素的尺寸变化
  • MutationObserver:监听目标元素的 DOM 更新
  • IntersectionObserver:监听目标元素与祖先元素的相交情况,用于判断元素的可见性
  • PerformanceObserver:监听性能事件的变化

3.1 IntersectionObserver

IntersectionObserver 是一种比 Element.getBoundingClientRect() 性能更好的检测元素相交的方法,使用场景如下:

  • 图片懒加载——当图片滚动到可见时才进行加载
  • 内容无限滚动——也就是用户滚动到接近内容底部时直接加载更多,而无需用户操作翻页,给用户一种网页可以无限滚动的错觉
  • 检测广告的曝光情况——为了计算广告收益,需要知道广告元素的曝光情况
  • 在用户看见某个区域时执行任务或播放动画

下面是一个简单的示例:

<html>
  <head>
    <style>
      .box {
        height: calc(100vh - 16px);
        overflow: auto;
      }
      .content {
        height: 100vh;
      }
      .target {
        height: 300px;
      }
    </style>
  </head>
  <body>
    <div class="box">
      <div class="content"></div>
      <div class="target">目标元素</div>
    </div>
​
    <script>
      // 收到通知的回调。创建观察者时,默认会执行一次回调。
      function callback(entries) {
        entries.forEach((entry) => {
          // 当目标元素可见时
          if (entry.isIntersecting) {
            console.log('目标元素出现了');
          }
        });
      }
      let options = {
        // 目标元素的祖先节点(必须支持滚动条),默认为 html 元素。
        root: document.body,
        // 祖先节点的 margin,用于扩大祖先节点的范围
        rootMargin: '0px',
        // 目标元素在祖先节点中的可见大小与实际大小的比例,每次经过这个比例时,观察者都会发出通知。threshold 也可以是数组。
        threshold: 0.5
      };
      // 创建观察者
      let observer = new IntersectionObserver(callback, options);
      // 观察目标元素
      observer.observe(document.querySelector('.target'));
    </script>
  </body>
</html>

其中,options.root 必须是目标元素的祖先节点,并且支持滚动条。

JS 机制

1 模块机制

模块就是可以按需导入的代码块。

1.1 模块分类

模块按照语言规范可以分为以下几种:

第一种,CommonJS,是 Node 环境提供的模块规范,使用 module.exportsrequire() 语法,是一种运行时加载模块的机制。

第二种,AMD,是使用 RequireJS 库实现的模块规范,使用 define()require() 语法,是浏览器环境中标准规范出来之前的替代方案。

第三种,ES Module,是浏览器环境提供的标准模块规范,使用 exportimport 语法,是一种编译时加载模块的机制。

模块按照使用范围可以分为以下几种:

  • 本地模块:本地代码中编写的模块
  • 公共模块:node_modules 中的模块

1.2 动态加载

如果希望在使用模块时才加载模块,从而提升性能,那么可以使用动态加载,比如:

import('/tool.js')
  .then((module) => {
    // Do something with the module.
  });

2 事件循环

2.1 内存模型

JS 的内存模型如下:

  • 堆:存放对象

  • 栈:存放正在调用的函数(包含参数和局部变量)

  • 队列:存放待处理的消息(等待调用的函数),分为两种:

    • 宏任务:setTimeout()、setInterval()、setImmediate()
    • 微任务:promise.then()、promise.catch()、new MutaionObserver()、process.nextTick()。微任务比宏任务优先执行。

2.2 事件循环

事件循环是指循环处理队列中的消息。具体流程如下:

  • 遇到同步代码时,会放入栈中并且立即执行
  • 遇到宏任务代码时,会在定时器线程(属于浏览器,不属于 JS)中执行计时任务,到时间后放入宏任务队列中
  • 遇到微任务代码时,会直接放入微任务队列中
  • 当所有的同步代码执行完,先从微任务队列中依次取出任务并执行,再从宏任务队列中依次取出任务并执行。

下面是一个简单的例子:

setTimeout(() => console.log(1), 0);
new Promise((r, j) => {
  console.log(2);
  r();
}).then((r) => console.log(3));
console.log(4);
​
// 2
// 4 
// 3
// 1

2.3 并发模型

JS 一般通过 setTimeout() 来执行异步任务,该方法接受两个参数:

  • handler:待处理的消息
  • timeout:消息加入队列的延迟时间

其中,timeout 参数的默认值为 0,代表该消息立即加入宏任务队列,但是并不意味该消息会立即执行,因为需要等待栈和微任务队列先执行完。下面是一个简单的示例:

console.log('这是开始');
setTimeout(() => console.log('这是来自第一个回调的消息'));
console.log('这是一条消息');
setTimeout(() => console.log('这是来自第二个回调的消息'), 0);
console.log('这是结束');
​
// "这是开始"
// "这是一条消息"
// "这是结束"
// "这是来自第一个回调的消息"
// "这是来自第二个回调的消息"

注意: 按照 W3C 的标准,当 setTimeout() 嵌套达到 5 层时,timeout 的最小值为 4ms。

2.4 多个运行时

每个网页、web worker、跨域 iframe 都有自己的运行时(独立的堆、栈、队列),不同的运行时只能同 postMessage 进行通信。

JS 常见问题

1 移动端适配

1.1 滑动穿透

滑动穿透:当移动端有fixed蒙层时,在蒙层上滑动半透明区域,会滑动蒙层下的内容。

解决方案:

/**
 * 解决滑动穿透的问题
 * @param show 是否显示蒙层
 */
function initScrollPenetrate(show) {
  document.documentElement.style.overflow = show ? 'hidden' : 'auto';
  document.documentElement.style.height = show ? '200px' : 'auto';
  document.body.style.overflow = show ? 'hidden' : 'auto';
  document.body.style.height = show ? '200px' : 'auto';
}

在iOS上,当fixed蒙层有滑动列表,并且H5的页面可以回弹时,如果从滑动列表滑到了半透明区域,那么下次滑动滑动列表时,会滑动蒙层下的内容,必须点击fixed蒙层的滑动列表后才能滑动。

注意:H5的页面回弹问题(页面滑动到底部后还可以滑动)需要native解决。

1.2 禁止系统键盘

禁止系统键盘需要让输入框失去焦点,使用:document.activeElement.blur()

onClick: (e) => {
    document.activeElement.blur();  //禁止系统键盘
    this.setState({showCar: 1});
},
onFocus: (e) => {
    document.activeElement.blur();
    this.setState({showCar: 1}, ()=> {
        document.activeElement.scrollIntoViewIfNeeded()
    });
},
onBlur: (e)=> {
    document.activeElement.scrollIntoViewIfNeeded()
}