抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

复习原生 js 知识

1. this 指向问题里的 call apply bind

call

call是一个方法,它的作用是改变函数的this指向。第一个参数是用于替换this的值,后续的参数是传递给函数的参数。例如:

1
2
3
4
5
6
7
function showName(age, job) {
console.log(`My name is ${this.name}, I'm ${age} years old and I am a ${job}.`);
}

let person = { name: 'Tom' };

showName.call(person, 25, 'engineer'); // 输出: My name is Tom, I'm 25 years old and I am a engineer.

call会立即执行函数。

apply

apply方法的作用和call类似,都是用于改变函数的this指向,不过apply接收的参数是一个数组(或类数组对象),第一个参数同样是用于替换this的值,第二个参数是传递给函数的参数数组。例如:

1
2
3
4
let person = { name: 'Tom' };
let args = [25, 'engineer'];

showName.apply(person, args); // 输出: My name is Tom, I'm 25 years old and I am a engineer.

call一样,apply也会立即执行函数。

bind

bind方法也是用于改变函数的this指向,它的使用方法和call类似,第一个参数是用于替换this的值,后续的参数是传递给函数的参数。不过,不同于callapplybind不会立即执行函数,而是返回一个新的函数。例如:

1
2
3
4
5
let person = { name: 'Tom' };

let newShowName = showName.bind(person, 25, 'engineer');

newShowName(); // 输出: My name is Tom, I'm 25 years old and I am a engineer.

总结:

  • callapply的主要作用都是改变函数的this指向,并立即执行函数。它们的区别主要在于参数的传递方式,call是将参数依次传入,而apply则是以数组形式传入参数。

  • bind方法也可以改变函数的this指向,但不同的是,它会返回一个新的函数,可以在需要的时候再去调用这个新函数。

2. 事件等级

DOM 0 级事件

DOM 0 级事件又称为原始事件模型,其事件绑定方法非常简单,直接在 HTML 元素上通过事件属性(如 onclick、onload、onmouseover 等)绑定 JavaScript 函数,或者在 JavaScript 代码中通过 JavaScript 对象的事件属性进行绑定。

例如:

1
2
3
4
5
6
7
8
// 通过 HTML 属性直接指定
<button onclick="console.log('Button clicked!')">Click me</button>;

// 通过 JavaScript 指定
let button = document.getElementById('myButton');
button.onclick = function () {
console.log('Button clicked!');
};

注意,在使用 DOM 0 级事件时,同一个事件只能绑定一个处理函数,多次设置会覆盖之前的处理器。

DOM 2 级事件

DOM 2 级事件提供了更多的事件类型和更丰富的事件处理方式。事件绑定主要使用 addEventListenerremoveEventListener 方法。

addEventListener 方法接受三个参数:事件名称,事件处理函数,和一个布尔值(可以指定事件处理器在捕获阶段或者冒泡阶段执行)。

例如:

1
2
3
4
5
6
7
8
9
let button = document.getElementById('myButton');

// 添加事件处理器
button.addEventListener('click', function () {
console.log('Button clicked!');
});

// 移除事件处理器
button.removeEventListener('click', handler);

不同于 DOM 0 级事件,DOM 2 级事件可以为同一个事件绑定多个处理函数,这些函数将按照绑定顺序依次执行。同时,可以通过 removeEventListener 移除绑定的事件处理函数。

DOM 3 级事件

DOM 3 级事件在 DOM 2 的基础上,引入了更多的事件类型,例如键盘事件、鼠标滚轮事件等。有以下常见的几类:

  • 鼠标事件:click、dblclick、mousedown、mouseup、mousemove、mouseover、mouseout、mouseenter、mouseleave 等。
  • 键盘事件:keydown、keyup、keypress。
  • 表单事件:focus、blur、change、submit。
  • 窗口事件:scroll、resize、load、unload。

3. JavaScript 继承

在 JavaScript 中,有多种方法实现对象的继承,其中包括原型链继承、组合继承、原型式继承、寄生式继承、寄生组合式继承,以及 ES6 中引入的基于 class 关键字的继承。

原型链继承

JavaScript 的每个对象都有一个指向它的原型(prototype)的链接。当试图访问一个对象的属性时,如果对象内部没有这个属性,那么 JavaScript 就会去对象的原型上找这个属性,这个过程叫做原型链查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Parent() {
this.parentValue = 'parent';
}

Parent.prototype.getParentValue = function () {
return this.parentValue;
};

function Child() {
this.childValue = 'child';
}

// Child 继承自 Parent
Child.prototype = new Parent();

let child = new Child();
console.log(child.getParentValue()); // 'parent'

缺点:父类的引用属性会被所有实例共享,一个实例修改了父类的引用属性,其他实例的这个属性也会被修改。

组合继承(经典继承)

组合继承是 JavaScript 最常用的继承模式。思路是使用原型链实现对原型属性和方法的继承,通过构造函数来实现对实例属性的继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Parent(value) {
this.parentValue = value;
}

Parent.prototype.getParentValue = function () {
return this.parentValue;
};

function Child(value, childValue) {
Parent.call(this, value); // 继承实例属性,第一次调用 Parent()
this.childValue = childValue;
}

// 继承方法
Child.prototype = new Parent(); // 第二次调用 Parent()
Child.prototype.constructor = Child;

let child = new Child('parent', 'child');
console.log(child.getParentValue()); // 'parent'

原型式继承

原型式继承的思路是基于已经存在的对象创建新对象,同时还不必因此创建自定义类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function object(o) {
function F() {}
F.prototype = o;
return new F();
}

let parent = {
parentValue: 'parent',
getParentValue: function () {
return this.parentValue;
},
};

let child = object(parent);
console.log(child.getParentValue()); // 'parent'

ES5 通过新增 Object.create() 方法规范化了原型式继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
var parent = {
name: 'parent',
getName: function () {
return this.name;
},
sayHello: function () {
console.log("Hello, I'm " + this.name);
},
};

// 创建新对象,并继承于parent对象
var child = Object.create(parent);

console.log(child.getName()); // 输出 "parent"
child.sayHello(); // 输出 "Hello, I'm parent"

// 可以给新对象添加新的属性或者覆盖继承来的属性
var child2 = Object.create(parent, {
name: {
// 覆盖 name 属性
value: 'child2',
enumerable: true,
writable: true,
configurable: true,
},
age: {
// 新增 age 属性
value: 10,
enumerable: true,
writable: true,
configurable: true,
},
});

console.log(child2.getName()); // 输出 "child2"
child2.sayHello(); // 输出 "Hello, I'm child2"
console.log(child2.age); // 输出 10

寄生式继承

寄生式继承的思路是创建一个用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。

1
2
3
4
5
6
7
8
9
10
11
function createAnother(original) {
var clone = object(original);
clone.sayHi = function () {
console.log('hi');
};
return clone;
}

let child = createAnother(parent);
console.log(child.getParentValue()); // 'parent'
child.sayHi(); // 'hi'

寄生组合式继承

寄生组合式继承是将寄生式继承和组合继承进行组合应用的方法。这种类型的继承效率较高,是 JavaScript 中最理想的继承范式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Parent(value) {
this.parentValue = value;
}

Parent.prototype.getParentValue = function () {
return this.parentValue;
};

function Child(value, childValue) {
Parent.call(this, value);
this.childValue = childValue;
}

// 寄生组合式继承的核心
(function () {
// 创建一个没有实例方法的 "类"
var Super = function () {};
Super.prototype = Parent.prototype;
// 让子类的原型等于 "类" 的实例, 实现继承
Child.prototype = new Super();
})();

let child = new Child('parent', 'child');
console.log(child.getParentValue()); // 'parent'

ES6 的 class 继承

ES6 中,可以使用 class 关键字来定义类,并通过 extendssuper 关键字来实现类的继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Parent {
constructor(value) {
this.parentValue = value;
}

getParentValue() {
return this.parentValue;
}
}

class Child extends Parent {
constructor(value, childValue) {
super(value); // 调用父类的 constructor(value)
this.childValue = childValue;
}
}

let child = new Child('parent', 'child');
console.log(child.getParentValue()); // 'parent'

class 继承的背后其实是原型链继承和构造函数继承的组合,是语法糖。

4. 事件循环

JavaScript 是单线程语言,为了实现执行异步操作,所以引入了事件循环(Event Loop)。它是一个持续运行的过程,可以理解为一个实际的循环,它在等待事件发生时继续运行。

宏任务和微任务

JavaScript 的任务可以分为宏任务和微任务

  • 宏任务:可以理解为需要在一次事件循环中全部执行完毕的任务,例如setTimeoutsetIntervalsetImmediate(Node.js 环境)、I/OUI rendering等。

  • 微任务:可以理解为需要在当前任务执行结束后立即执行的任务,例如Promiseprocess.nextTick(Node.js 环境)、MutationObserver等。

事件循环过程

  1. 执行同步代码,这属于一个宏任务。

  2. 执行完所有同步代码后,执行下一个宏任务前,在下一个宏任务开始前,会执行所有的微任务。

  3. 当所有微任务执行完毕后,有可能需要进行 UI 渲染。

  4. 然后继续下一个宏任务,执行对应的任务队列。

  5. 循环上述步骤。

经典题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
console.log('1');

setTimeout(function () {
console.log('2');
process.nextTick(function () {
console.log('3');
});
new Promise(function (resolve) {
console.log('4');
resolve();
}).then(function () {
console.log('5');
});
});

new Promise(function (resolve) {
console.log('7');
resolve();
}).then(function () {
console.log('8');
});

async function async1() {
console.log('6');
await async2();
console.log('9');
}
async function async2() {
console.log('10');
}

process.nextTick(function () {
console.log('11');
});

async1();

new Promise(function (resolve) {
console.log('12');
resolve();
}).then(function () {
console.log('13');
});

console.log('14');

// 答案1 7 6 10 12 14 8 11 9 13 2 4 5 3

注意,await 后面的东西相当于new Promise(function(resolve) {console.log('7');resolve();里面的console.log('7')

5. 模块化

AMD (Asynchronous Module Definition)

AMD 是 “Asynchronous Module Definition” 的缩写,意思就是 “异步模块定义”。它采用异步方式加载模块,模块的加载不影响后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。AMD 的代表实现是 RequireJS。

1
2
3
4
// 定义一个模块
define('module', ['dep1', 'dep2'], function (dep1, dep2) {
return someExportedValue;
});

CMD (Common Module Definition)

CMD 是 “Common Module Definition” 的缩写,也就是 “通用模块定义”。CMD 采用的是异步加载模块,允许模块和模块之间有依赖关系,也支持就近依赖,只有在用到某个模块的时候再去加载那个模块。CMD 的代表实现是 SeaJS。

1
2
3
4
define(function (require, exports, module) {
var dep1 = require('dep1');
exports.action = function () {};
});

CommonJS

CommonJS 是 Node.js 模块系统的基础,它用于服务器。每个文件是一个模块,通过 require 来加载模块,通过 exportsmodule.exports 来导出模块。CommonJS 使用同步加载模块的方式。

1
2
var dep1 = require('dep1');
exports.someMethod = function () {};

ES6 Modules

ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。模块不是对象,import 命令会被 JavaScript 引擎静态分析,生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块去取值。

1
2
import { dep1, dep2 } from 'module-name';
export function someMethod() {}

6. 迭代器和生成器

迭代器 (Iterator)

迭代器是一种特殊对象,它包含一个叫做 next 的方法。这个方法返回一个结果对象,这个对象有两个属性:

  • value:下一个值
  • done:布尔类型,如果没有更多的数据则为 true,否则为 false。

迭代器的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let iterator = {
current: 1,
next() {
let result = { value: null, done: true };
if (this.current <= 3) {
result.value = this.current;
result.done = false;
this.current++;
}
return result;
},
};

console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
console.log(iterator.next().done); // true

生成器(Generator)和 yield 关键字

生成器是一种可以返回多个连续值的函数。换句话说,一个生成器就像一个工厂,制造一系列的值。生成器函数通过 * 符号来定义,并且可以通过 yield 关键字来产生一个值。

当一个生成器函数被调用时,它返回一个特殊类型的迭代器,称为生成器对象。这个对象也包含一个 next 方法,但是可以在生成器函数内部控制这个方法的行为。

生成器的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 例子1
function* generator() {
yield 1;
yield 2;
yield 3;
}

let iterator = generator();
console.log(iterator.next().value); // 1
console.log(iterator.next().value); // 2
console.log(iterator.next().value); // 3
console.log(iterator.next().done); // true

//例子2
function* generator() {
const dataIn = yield 'This will be replaced by next() argument';
console.log(dataIn);
}

const iter = generator();
console.log(iter.next()); // 输出 { value: 'This will be replaced by next() argument', done: false }
iter.next('Hello, generator!'); // 输出 'Hello, generator!'

每次调用生成器对象的 next 方法,就会执行生成器函数直到遇到一个 yield 语句。然后,yield 的值就是 next 方法返回的值。当没有更多的 yield 语句时,done 属性的值就会变成 true。

next() 函数还可以接收一个参数,这个参数可以作为 yield 语句的结果返回给生成器内部。这个特性可以使得生成器和外部环境进行双向的数据交换。比如例子 2 里调用 iter.next('Hello, generator!'),传入的参数 'Hello, generator!' 会作为上一次 yield 语句的结果,赋值给 dataIn 变量。然后生成器恢复执行,直到遇到下一个 yield 语句,或者结束。

评论