第一部分
async await 是什么?它有哪些作用?
扩展问题
- 你怎么看” 回调地狱 “问题?怎么解决?为什么?
- async await 有哪些优点和缺陷?
- await/async 的原理是什么?
async await 是 es7 里面的新语法,它的作用就是 async 用于声明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。它可以很好的替代 promise 中的 then。
async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
TIP
async 和 await 对于解决” 回调地狱 “问题十分有效,其本质是 Promise 的语法糖。但是其本身也存在一些问题,一种就是 await
和 async
的连续传播问题,原因是 await
只能用在 async
函数中;另外一种就是顶层 await
问题(破坏了前述的规则),这种问题目前已经被解决,参见 javascript - How can I use async/await at the top level? - Stack Overflow。
The await operator is used to wait for a Promise and get its fulfillment value. It can only be used inside an async function or at the top level of a module. 参见 await - JavaScript | MDN。
另外需要注意的是,async/await 并不是用来替代 Promise 的。请注意,只在适当的场合使用它,因为它会阻塞函数中其余部分代码的执行,使函数的执行效果变为类似于同步执行。对 async/await 的过分依赖(源于 async/await 的传播性)会牺牲掉 Promise 异步的特性,是程序丢失掉异步的高效,降低程序的执行效率。最为需要注意的一个问题便是,不该 await
的方法就不要 await
。
await/async 的原理
async/await
最鲜明的特点就是:在 async
函数中, await
使得函数有了” 暂停 “和” 继续 “的意味。这恰恰是生成器的特点,只是生成器要更加的强大。 async/await
为什么会有这样的特点呢?其原理还是 Promise。
在 async
函数中,主线程在遇到 await
之后,会将 await
的任务和该函数中剩余的部分包装为一个 Promise,并将这个 Promise 推入到微任务队列中,然后主线程就从该 async
函数中返回了,继续执行剩余的代码。当程序堆栈为空时(主线程空闲),主线程会从微任务队列中取出前述的 Promise 任务进行执行(前提是该 Promise 不是 pending 状态,即 await
的任务已经完成,否则就推迟到下一次主线程回调)。由此可见 await
之前的代码的执行行为是正常的,而后续的执行其实是 Promise 的再包装, async/await
实际上是 Promise 的语法糖。详见图解 JavaScript 之 Promises 和 Async/Await。
Promise 才应该是主角。” 回调地狱 “固然是问题,但是这不应该是” 回调 “的锅。” 回调 “在很大程度上提升了 JavaScript 的生产力,同时也是函数式编程一大利器。我们应该充分发挥 Promise 的效率,利用好事件循环和微任务,在这个方面, single-spa
给我们提供了很好的范例,参见微前端基础与 single-spa 初探。
怎么配置使用 async/await?
async/await
是 ES7 标准中的新特性。如果是使用 React 官方的脚手架(Create React App)创建的项目,就可以直接使用。如果是在自己搭建的 webpack 配置的项目中使用,可能会遇到 regeneratorRuntime is not defined
的异常错误。那么我们就需要引入 babel,并在 babel 中配置使用 async/await
。可以利用 babel 的 @babel/transform-runtime
插件来转换其成为浏览器支持的语法,虽然没有性能的提升,但对于代码编写体验要更好。
配置过程(支持 Babel 7):
安装依赖:
bash
npm install --save @babel/runtime
npm install --save-dev @babel/plugin-transform-runtime
1
2
2
配置 .babelrc
文件:
js
{
"presets": ["@babel/preset-env"],
"plugins": [
["@babel/transform-runtime"]
]
}
1
2
3
4
5
6
2
3
4
5
6
参考:
常用的数组方法有哪些?
扩展问题
- JavaScript 中数组是什么原理?
- JavaScript 中的数组为什么能够自动扩容?具体是怎么自动扩容的?
- JavaScript 中的数组是以怎样的数据结构储存的?
- 怎么样提升数组的存取效率?为什么?
- JavaScript 中的数组是如何存取的?
- 聊一聊你对 JavaScript 的数组的理解,它的高效体现在哪?有哪些缺陷?
- JavaScript 数组中某个数组方法的原理是什么?
常用的数组方法,略。参见:Array - JavaScript | MDN。
JavaScript 中数组的原理
JavaScript 中的数组和编译型语言,如 Java、C++ 中的数组完全不是同一个概念。JavaScript 中的数组是一个及其复杂的数据结构,其本质是 Object。如下:
js
Array.prototype.__proto__ === Object.prototype // true
1
数组在 V8 中是 FixedArray
,数组元素称之为 element
。在 Object
中,对象的属性必须是 String,如果不是 String 需要转化为 String。Number 和 Symbol 在作为属性时便是如此。由此,如果属性是可以转成数组的,如数字 1 或者字符 1
,则存为 Element,否则就存为 Property。Properties 以非连续内存储存,因此不可以下标取值;Elements 以连续内存储存,因此可以下标取值。另外 Elements 有 length 属性。因此,Array 的本质是 Object,同时 Array 继承(原型链继承不同于传统继承,此处指的是 Object 在 Array 原型链的上游)自 Object。
Array 的数据结构:
我们可能注意到了 JavaScript 中的 Array 和传统 Array 相比具有如下神奇的特性:
- 不需要初始化长度,数组具有动态扩容机制。
- 不需要限定元素类型,数组元素支持动态类型。
大家可能会觉得这是动态语言的特征,但是其背后的原理是什么呢?
JavaScript 的数组分为快数组和慢数组,其中快数组是具有动态扩容机制的真正的数组,其内存是连续存储的,访问速度较快;慢数组则是 HashMap,这对于稀疏数组比较有效,节省了存储空间,但是牺牲了访问速度。在 Array 中,未填充值的元素称之为 Hole(空洞),打印出来往往是 empty
(注意,不是指 undefined
)。很显然,如果 Array 较为稀疏,即 Hole 比较多,使用慢数组比较有效率,毕竟内存代价也是比较昂贵的;反之,当 Array 较为紧密时,即 Hole 比较少,使用快速组则能够充分发挥数组快速访问的特性。V8 中很好的平衡了数组中空间和时间占用的开销,帮助我们自动的进行快慢数组的切换。
注意,快慢数组的切换是有一定复制元素的成本的,因此,我们应该减少这种情况频繁地发生。
快数组是如何存取的?快数组可以通过下标快速存取。
慢数组是如何存取的?慢数组中的哈希表是通过数组模拟的。在存值时,首先 key 值经过哈希处理和一定的变换机制(取低两位)找到该元素的起始存放位置,如果该位置是空的,则直接存放在该位置,如果该位置已经被占用,则继续向后寻找,直到找到一个空位置进行存储;在取值时,同样方法找到该元素的起始查找位置,匹配 key 值并且不断向后查找,直至找到该元素。
什么是原型链?
扩展问题
- 原型链的原理是什么?
- 为什么使用原型链?有什么好处?
- 原型链继承和经典继承有什么不同?
每一个实例对象上有一个 __proto__
属性,指向构造函数的原型对象,构造函数的原型对象(prototype)也是一个对象, 也有 __proto__
属性,这样一层一层往上找的过程就形成了原型链。
原型链的历史渊源和原理
早些年间浏览器只能浏览网页内容,而不能进行用户交互,交互主要靠服务器,因此给服务器造成了过载。JavaScript 和 DOM/BOM 的出现就是为了解决浏览器交互性问题而生的。
JavaScript 的设计思想收到了当时流行的 Java 的影响,采用了对象和继承的机制(这对于 CPP 来说也是比较友好的),但是重新设计了继承机制。这种继承机制的灵感来源于 Java 和 JS 两者都有构造函数的共同点,构造函数即为实例对象的构造对象。
这其中存在的问题就是实例对象间无法共享公共属性,因此要设计一个对象专门用来存储对象共享的属性,即为原型对象。原型继承机制给构造函数增加 prototype
属性以指向原型对象,把所有实例对象共享的属性和方法都放在原型对象中,不需要共享的属性和方法放在构造函数中。实例对象通过构造函数创建时,其 __proto__
属性就会指向原型对象。这种通过原型建立起来的属性和方法的继承关系即为原型链。
实例对象通过原型链继承其上游的原型对象的属性和方法。实例对象的属性被应用时,先从实例对象自身查找该属性,如果无法找到,就依原型继承关系向原型链上游查找,找到原型链的尽头 —— Object.prototype.__proto__===null
。
其中: Foo
为构造对象, Foo.prototype
为 Foo
的原型对象, b
和 c
为 Foo
的实例对象。原型关系如下:
js
b.__proto__ === c.__proto__ === Foo.prototype
Foo.__proto__ === Function.prototype
Foo.prototype.__proto__ === Object.prototype
Function.__proto__ === Object.prototype
Object.__proto__ === null
1
2
3
4
5
2
3
4
5
其规律为: 继承者.__proto__===被继承者.prototype
。参见:图解 JavaScript 之原型继承
原型链查找的效率问题
了解原型继承和属性查找的工作方式对开发者来说很重要,但也是必不可少的,因为它对 JavaScript 的性能影响很重要。正如 V8 的文档中提到的,大多数 JavaScript 引擎使用类似字典的数据结构来存储对象属性。因此,每个属性的访问都需要在该数据结构中进行动态查找该属性。这种方法使得在 JavaScript 中访问属性通常比访问 Java 和 Smalltalk 等编程语言中的实例变量慢得多。参见 Javascript Prototype & Scope Chains: What You Need to Know。
但是也不必太担心,因为在大多数情况下,在实例对象上可以命中属性。即使需要查找原型链,其速度也不会对程序的性能造成显著的影响。
参见:
参考:
JavaScript 为什么使用原型链?相比于 CBP(class-based programming)实现的 OOP,prototype-based programming(PBP)有什么优势和缺陷?
经典继承与原型继承的区别?
关于原型链的一些有趣的问题:
- javascript - Why is prototype function 40x slower than the default declared function? - Stack Overflow
- javascript - Defining methods via prototype vs using this in the constructor - really a performance difference? - Stack Overflow
什么是闭包?手写一个闭包函数?闭包有哪些优缺点?
扩展问题
- 闭包的原理是什么?
- 你对闭包有什么理解?
- 你所知道闭包有哪些应用?
闭包(closure)指有权访问另一个函数作用域中变量的函数。简单理解就是 ,一个作用域可以访问另外一个函数内部的局部变量。例如:
js
function fn() {
var num = 10
function fun() {
console.log(num)
}
return fun
}
var f = fn()
f()
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
特点:从内部函数访问外部函数的作用域;容易造成内层泄露,因为闭包中的局部变量永远不会被回收。
闭包
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。
性能考量:
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。原因是这将导致每次构造器被调用时,方法都会被重新赋值一次(也就是说,对于每个对象的创建,方法都会被重新赋值)。
闭包的应用场景
- 用闭包模拟私有方法:如常用的防抖和节流函数。
- 利用闭包编写组件。React 中的函数式组件(FC)正是利用了闭包” 共享作用域 “的特性。组件的状态(State 和 Context)和属性(Props)是通过闭包分发的。React 在渲染组件时执行闭包,保证了组件中的状态和行为不会被泄露和污染。当然,应用中大量的组件也会带来一定的性能负担。参见基于属性和状态的单向数据流的模型。
- 闭包陷阱:在 React 中经常会遇到闭包陷阱,尤其在
useEffect
中。由于 React 不支持自动解析副作用的依赖,因此很多函数不得不放到useEffect
内部,导致经常出现意想不到的闭包问题。解决闭包问题的常见方法是使用useRef
。
常见的继承有哪些?
原型链继承
特点:
- 实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。
缺点:
- 新实例无法向父类构造函数传参。
- 继承单一。
- 所有新实例都会共享父类实例的属性。
js
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){}
SubType.prototype = new SuperType();
1
2
3
4
5
6
2
3
4
5
6
借用构造函数继承
重点: 用 .call()
和 .apply()
将父类构造函数引入子类函数。
特点:
- 只继承了父类构造函数的属性,没有继承父类原型的属性。
- 可以继承多个构造函数属性。(解决了原型链继承缺点 1、2、3)
- 在子实例中可向父实例传参。
缺点:
- 只能继承父类构造函数的属性。
- 无法实现构造函数的复用。
- 每个新实例都有父类构造函数的副本冗余。
js
function SuperType(){
this.color=["red","green","blue"];
}
function SubType(){
//继承自SuperType
SuperType.call(this);
}
1
2
3
4
5
6
7
2
3
4
5
6
7
组合继承
组合原型链继承和借用构造函数继承的方法。
重点:结合了两种模式的优点,可以传参和复用。
特点:
- 可以继承父类原型上的属性,可以传参,可复用。
- 每个新实例引入的构造函数属性是私有的。
缺点: 调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
js
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
alert(this.name);
};
function SubType(name, age){
// 继承属性
// 第二次调用SuperType()
SuperType.call(this, name);
this.age = age;
}
// 继承方法
// 构建原型链
// 第一次调用SuperType()
SubType.prototype = new SuperType();
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
原型式继承
重点: 用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create () 就是这个原理。
特点: 类似于复制一个对象,用函数来包装。
缺点:
- 所有实例都会继承原型上的属性。
- 无法实现复用。
js
// ES5中存在Object.create()的方法,能够代替此方法。
function create(obj){
function F(){}
F.prototype = obj;
return new F();
}
1
2
3
4
5
6
2
3
4
5
6
class 类实现继承
通过 extends 和 super 实现继承。
寄生式继承
重点: 在原型式继承的基础上,增强对象,返回构造函数。
优点: 没有创建自定义类型,因为只是套了个壳子返回对象,这个函数顺理成章就成了创建的新对象。
缺点: 没用到原型,无法复用。
js
function createObject(original){
var clone = create(original); // 通过调用 object() 函数创建一个新对象
clone.sayHi = function(){ // 以某种方式来增强对象
alert("hi");
};
return clone; // 返回这个对象
}
1
2
3
4
5
6
7
2
3
4
5
6
7
ES5 继承和 ES6 继承的区别
ES5 的继承是先创建子类的实例对象,然后再将父类的方法添加到 this 上(Parent.call (this))。
ES6 的继承是先创建父类的实例对象 this,然后再用子类的构造函数修改 this。因为子类没有自己的 this 对象,所以必须先调用父类的 super () 方法,否则新建实例报错。
参见:
你如何实现 ES6 的 Class?
扩展问题
- Class 的原理是什么?如何实现?
实现代码如下:
js
const isFunction = (n) => typeof n === "function";
const extendProperties = (base, extend, cb) => {
for (let p in extend) {
if (extend.hasOwnProperty(p)) {
if (isFunction(cb)) {
cb(p);
} else {
base.prototype[p] = extend[p];
}
}
}
return base;
};
const Class = (() => {
function create(properties, parent) {
function _instance() {
if (isFunction(this.initialize)) this.initialize.apply(this, arguments);
}
function polymorph(thisFunction, parentFunction) {
return function () {
this.__parent = parentFunction;
const output = thisFunction.apply(this, arguments);
delete this.__parent;
return output;
};
}
if (parent) {
_instance.prototype = new parent.constructor();
extendProperties(_instance, parent);
}
extendProperties(_instance, properties, (p) => {
_instance.prototype[p] =
parent && isFunction(parent[p])
? polymorph(properties[p], parent[p])
: properties[p];
});
_instance.extend = function extend(properties) {
return create(properties, this.prototype);
};
return _instance;
}
return { 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
38
39
40
41
42
43
44
45
46
47
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
测试代码如下:
js
var Accommodation = Class.create({
isLocked: true,
isAlarmed: true,
lock: function () {
this.isLocked = true;
},
unlock: function () {
this.isLocked = false;
},
initialize: function () {
this.unlock();
},
});
var House = Accommodation.extend({
floors: 2,
lock() {
console.log("Number of floors locked:" + this.floors);
},
});
var myAccommodation = new Accommodation();
console.log(myAccommodation instanceof Accommodation); // true
console.log(myAccommodation instanceof House); // false
var myHouse = new House();
console.log(myHouse instanceof House); // true
console.log(myHouse instanceof Accommodation); // true
console.log(myHouse.isLocked); // false
myHouse.lock(); // Number of floors locked:2
console.log(myHouse.isLocked); // false
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
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
参见:
es6 有哪些新特性?
ES6 的新特性现在已经不是那么 “新” 了,参照 ES6 入门教程。
cookie 、localStorage 、 sessionStorage 之间有什么区别?
参见:
- https://www.xyass.com/blogs/cookies-and-localstorage-and-sessionstorage-a-detailed-explanation-and
- Difference Between Local Storage, Session Storage And Cookies - GeeksforGeeks
this 的指向有哪些?
总结一下:
- 普通函数中的 this 指向 window
- 定时器中的 this 指向 window
- 箭头函数中 this 指向取决于外部环境
- 事件中的 this 指向事件的调用者
- 构造函数中 this 和原型对象中的 this,都是指向构造函数 new 出来实例对象
- 类 class 中的 this 指向由 constructor 构造器 new 出来的实例对象
- 自调用函数中的 this 指向 window
谈谈你平时都用了哪些方法进行性能优化?
js 的执行机制是怎么样的?
js 是一个单线程、异步、非阻塞 I/O 模型、 event loop 事件循环的执行机制
所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。异步 任务指的是,不进入主线程、而进入 "任务队列"(task queue)的任务,只有 "任务队列" 通知主线程, 某个异步任务可以执行了,该任务才会进入主线程执行。
如何判断数据类型?各有什么优缺点?
symbol 的原理是什么?
promise 是什么?它有哪些作用?
箭头函数有哪些特征?它的原理是什么?
BFC 是什么?
BFC(会计格式化上下文),一个创建了新的 BFC 的盒子是独立布局的,盒子内元素的布局不会影响盒 子外面的元素。在同一个 BFC 中的两个相邻的盒子在垂直方向发生 margin 重叠的问题。
BFC 是值浏览器中创建了一个独立的渲染区域,该区域内所有元素的布局不会影响到区域外元素的布 局,这个渲染区域只对块级元素起作用。
call、apply、bind 三者的异同?
共同点:都可以改变 this 指向;
不同点: call 和 apply 会调用函数,并且改变函数内部 this 指向. call 和 apply 传递的参数不一样,call 传递参数使用逗号隔开,apply 使用数组传递 bind 不会调用函数,可以改变函 数内部 this 指向。应用场景
- call 经常做继承.
- apply 经常跟数组有关系。比如借助于数学对象实现数组最大值最小值
- bind 不调用函数,但是还想改变 this 指向。比如改变定时器内部的 this 指向。