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)和字母 a
f 或 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 操作。
- 箭头函数没有自己的
this,arguments,super或new.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 构造函数的过程中,会做如下操作:
- 创建一个空对象:{}。
- 给新对象添加
[[Prototype]]属性:指向对象类型(也叫构造函数)的 prototype; - 将函数的 this 指向新对象,然后运行函数
- 返回函数的 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 ==
== 的比较规则很复杂,具体如下:
-
如果遇到 null、undefined,那么它们只等于本身或对方,即 null == undefined
-
如果类型相同,那么比较值
- 如果遇到 NaN、0,那么需要特殊处理:NaN != NaN,+0 == -0
- 如果都是对象类型,那么比较值的引用地址
-
如果类型不同,那么先进行类型转换,再比较值
- 如果其中有 symbol,那么返回 false
- 如果其中有 boolean,那么先将 boolean 转为 number:true = 1,false = 0,然后再次进行宽松比较
- 如果是 string、number、bigint 的比较,那么先转为 number,然后再比较
- 如果是基本类型与对象类型的比较,那么先将对象类型转为基本类型,然后再比较
对象类型转为基本类型的方法如下:
- 如果对象有
[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.exports 和 require() 语法,是一种运行时加载模块的机制。
第二种,AMD,是使用 RequireJS 库实现的模块规范,使用 define() 和 require() 语法,是浏览器环境中标准规范出来之前的替代方案。
第三种,ES Module,是浏览器环境提供的标准模块规范,使用 export 和 import 语法,是一种编译时加载模块的机制。
模块按照使用范围可以分为以下几种:
- 本地模块:本地代码中编写的模块
- 公共模块: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()
}