Fork me on GitHub

前端话题

记录一些比较有意思的话题。

new操作符的工作原理

我们都知道 new 运算符是用来实例化一个类,从而在内存中分配一个实例对象。

1
2
3
4
5
6
7
let Person = function(name, age){
this.name = name;
this.age = age;
}
Person.prototype.getAge = function(){
return this.age;
}

PS:这里我先说明一下直接执行 Person 会返回 undefined,new Person(…) 会返回一个对象(即我们的this对象)。

调用构造函数实际上会经历以下4个步骤

(1) 创建一个新对象
(2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象即类的实例)
(3) 执行构造函数中的代码(即为这个新对象添加属性)
(4) 返回新对象
如果不明白,请看前辈整理的文章

JavaScript内部属性[[Scope]]与作用域链的理解

[[Scope]]属性

每一个 function 声明时都会有一个内部属性 [[Scope]],例如声明 foo 函数会创建一个 foo.[[Scope]] 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var a = 1;
function foo(){
...
}
// 当我们的 foo 函数创建时,它的作用域链中插入了一个全局对象GO(Global Object),包含全局所有定义的变量
// 伪代码
foo.[[Scope]] = {
GO: {
this: window ,
window: ... ,
document: ... ,
......
a: undefined, // 预编译阶段还不知道a值是多少
foo: function(){...},
}
}

执行环境

在函数执行时,会创建一个叫做执行环境/执行上下文(execution context,下文均用EC表示)的内部对象(独一无二)。

执行环境有以下特点

函数每次执行时的执行环境独一无二
多次调用同一函数就多次创建执行环境
并且函数执行完毕后,执行环境就会被销毁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// foo函数执行前,创建了执行期上下文(EC)
// 首先取得foo内部[[Scope]]属性保存的作用域链(复制)到 EC 的底部
// 然后foo函数执行前预编译产生了一个活动对象AO(Active Object),这个对象被推入EC作用域链的最前端
// 伪代码:foo函数预编译产生AO活动对象,挂载到foo中EC作用域链的最前端
foo.EC = {
AO: {
this: window,
arguments: [100,200],
x: 100,
y: 200,
b: undefined,
bar: function(){...}
},
GO: {
this: window ,
window: ... ,
document: ... ,
a: 1,
foo: function(){...},
......
}
}

案列分析

这里我们来看一个稍微复杂一点的场景

1
2
3
4
5
6
7
8
9
10
var a = 1;
function foo(x, y){
var b = 2;
function bar(){
var c = 3;
// console.log(a);
}
bar();
}
foo(100, 200);

foo函数在预编译阶段创建了bar函数,于是bar函数创建了属性[[Scope]],包含bar被创建的作用域中对象的集合,也就是复制了foo.EC
所以我们可以得到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 伪代码:bar函数创建产生[[Scope]]对象
// bar.[[Scope]] = foo.EC
bar.[[Scope]] = {
AO: {
this: window,
arguments: [100,200],
x: 100,
y: 200,
b: undefined,
bar: function(){...}
},
GO: {
this: window ,
window: ... ,
document: ... ,
a: 1,
foo: function(){...},
......
}
}

PS:由于bar函数是在foo函数执行时创建的,所以bar[[Scope]]=foo.EC
bar函数执行,过程同foo函数执行相近,整理出 bar.EC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bar.EC = {
AO: { // bar 产生的 AO
this: window,
arguments: [],
c: undefined,
},
AO: { // foo 产生的 EC
this: window,
arguments: [100,200],
x: 100,
y: 200,
b: 2,
bar: function(){...}
},
GO: { // foo 的 [[Scope]]
this: window ,
window: ... ,
document: ... ,
a: 1,
foo: function(){...},
......
}
}

作用域链

js引擎就是通过作用域链的规则来进行变量查找(准确的说应该是执行上下文的作用域链)
查找过程就拿上面的代码来说,比如说我在bar函数执行console.log(a);
那么bar函数执行时,js引擎想要打印a,于是就去作用域链上查找
第一层AO没有(bar运行时产生的)
第二层AO没有(foo运行时产生的)
第三层GO找到了变量a (foo定义是a为undefined,预编译时a被赋值为1)
于是返回了变量a的值
如果在bar函数中在创建一个der函数,der的EC又会是怎么样呢?读者自行脑补吧(大体思路类似)
[[Scope]]与作用域链

defer和async

四种组合关系

<script src="script.js"></script>

没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。

<script async src="script.js"></script>

有 async,加载和渲染后续文档元素的过程将和 script.js 的加载并行进行,且并立即执行。

<script defer src="script.js"></script>

有 defer,加载和渲染后续文档元素的过程将和 script.js 的加载并行进行,但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。

<script defer async src="script.js"></script>

同时存在时,async生效。

defer和async 共同点

defer 和 async 在内联脚本无作用。
在下载时和与HTML解析异步的,执行时阻塞HTML解析(包括没有defer 和 async属性的场景)。

defer和async的区别

async异步下载后立即执行(可能不按下载顺序执行,适用于无任何依赖的脚本)。
defer异步下载后等文档完成解析后,触发 DOMContentLoaded 事件前执行(安下载顺序执行,适用于有依赖关系的脚本)。
PS:CSS并行下载,JS串行下载,相对于HTML解析来说。
defer和async的区别

PWA

具体功能

可以添加至主屏幕
实现离线缓存功能
实现了消息推送

相关技术

App Manifest
Service Worker
Push && Notification(push: server 将更新的信息传递给 SW notification: SW 将更新的信息推送给用户)
讲讲PWA

jsEvent Loop机制

在JavaScript中,任务被分为Task(又称为MacroTask,宏任务)和MicroTask(微任务)两种。

MicroTask

process.nextTick(node独有), Promises, Object.observe(废弃), MutationObserver

MacroTask

script(同步代码), setTimeout, setInterval, setImmediate(node独有), I/O, UI rendering

执行顺序

script(同步代码) -> MicroTask -> MacroTask
在执行上面代码时有产生了一些 MicroTask 和 MacroTask 会挂起,在一下次Event Loop再触发,以此类推。
可以看看我之前的博客

通用 curry 实现

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
//柯里函数实质:传递给函数一部分参数来调用它,让它返回一个函数来处理剩余参数
function curry(fx) {
//要进行柯里化的函数的形参数量
let arity = fx.length;

return function f1() {
//第一次传入的参数数量
let args = Array.from(arguments);
//若传入的参数数量大于等于形参数量
if (args.length >= arity) {
return fx.apply(null,args)
}else{
let f2 = function() {
//如果只传入了一部分参数
let args2 = Array.from(arguments)
//判断是否所有参数都传完了,如果没有,不断concat新传的参数,然后执行f1函数
return f1.apply(null, args.concat(args2))
}
return f2
}

}
}

let add = (num1, num2, num3)=> num1 + num2 + num3;
console.log(curry(add)(1)(2)(3)) // 6

curry实现

通用的 compose 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let compose = function(...args) {
var len = args.length,
count = len - 1,
result;

return function f1(...args1) {
result = args[count].apply(this, args1);
if (count <= 0) {
count = len - 1;
return result;
} else {
count--;
return f1.call(null, result);
}
}
}

关于javascript函数式编程中compose的实现

参考文档:
详解Javascript中new()到底做了些什么?

-------------本文结束感谢您的阅读,如果本文对你有帮助就记得给个star-------------
Donate comment here