执行上下文
当执⾏ JS 代码时,会产⽣三种执⾏上下⽂
全局执⾏上下⽂
函数执⾏上下⽂
eval 执⾏上下⽂
每个执⾏上下⽂中都有三个重要的属性
变量对象(VO),包含变量、函数声明和函数的形参,该属性只能在全局上下⽂中访问
作⽤域链(JS 采⽤词法作⽤域,也就是说变量的作⽤域是在定义时就决定了)
this
var a = 10
function foo(i) {
var b = 20
}
foo()
对于上述代码,执⾏栈中有两个上下⽂:全局上下⽂和函数 foo 上下⽂
。
stack = [
globalContext,
fooContext
]
对于全局上下⽂来说,VO ⼤概是这样的
globalContext.VO === globe
globalContext.VO = {
a: undefined,
foo: <Function>,
}
对于函数 foo 来说,VO 不能访问,只能访问到活动对象(AO)
fooContext.VO === foo.AO
fooContext.AO {
i: undefined,
b: undefined,
arguments: <>
}
// arguments 是函数独有的对象(箭头函数没有)
// 该对象是⼀个伪数组,有 `length` 属性且可以通过下标访问元素
// 该对象中的 `callee` 属性代表函数本身
// `caller` 属性代表函数的调⽤者
对于作⽤域链,可以把它理解成包含⾃身变量对象和上级变量对象的列表,通过 [[Scope]]
属性查找上级变量
fooContext.[[Scope]] = [
globalContext.VO
]
fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
fooContext.Scope = [
fooContext.VO,
globalContext.VO
]
接下来让我们看⼀个⽼⽣常谈的例⼦, var
b() // call b
console.log(a) // undefined
var a = 'Hello world'
function b() {
console.log('call b') }
想必以上的输出⼤家肯定都已经明⽩了,这是因为函数和变量提升的原因。通常提升的解释
是说将声明的代码移动到了顶部,这其实没有什么错误,便于⼤家理解。但是更准确的解释
应该是:在⽣成执⾏上下⽂时,会有两个阶段。第⼀个阶段是创建的阶段(具体步骤是创建
VO),JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数
的话会将整个函数存⼊内存中,变量只声明并且赋值为 undefined,所以在第⼆个阶段,也就
是代码执⾏阶段,我们可以直接提前使⽤。
在提升的过程中,相同的函数会覆盖上⼀个函数,并且函数优先于变量提升
b() // call b second
function b() {
console.log('call b fist') }
function b() {
console.log('call b second') }
var b = 'Hello world'
var 会产⽣很多错误,所以在 ES6中引⼊了 let 。 let 不能在声明前使⽤,但是这并不是
常说的 let 不会提升, let 提升了声明但没有赋值,因为临时死区导致了并不能在声明前
使⽤。
对于⾮匿名的⽴即执⾏函数需要注意以下⼀点
var foo = 1 (function foo() {
foo = 10
console.log(foo)
}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }
因为当 JS 解释器在遇到⾮匿名的⽴即执⾏函数时,会创建⼀个辅助的特定对象,然后将函数
名称作为这个对象的属性,因此函数内部才可以访问到 foo ,但是这个值⼜是只读的,所以
对它的赋值并不⽣效,所以打印的结果还是这个函数,并且外部的值也没有发⽣更改。
specialObject = {};
Scope = specialObject + Scope;
foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}
delete Scope[0]; // remove specialObject from the front of scope chain
闭包
闭包的定义很简单:函数 A 返回了⼀个函数 B,并且函数 B 中使⽤了函数 A 的变量,函数 B
就被称为闭包。
function A() {
let a = 1
function B() {
console.log(a)
}
return B }
你是否会疑惑,为什么函数 A 已经弹出调⽤栈了,为什么函数 B 还能引⽤到函数 A 中的变
量。因为函数 A 中的变量这时候是存储在堆上的。现在的 JS 引擎可以通过逃逸分析辨别出
哪些变量需要存储在堆上,哪些需要存储在栈上。
经典⾯试题,循环中使⽤闭包解决 var 定义函数的问题
for ( var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
⾸先因为 setTimeout 是个异步函数,所有会先把循环全部执⾏完毕,这时候 i 就是 6
了,所以会输出⼀堆 6。
解决办法两种,第⼀种使⽤闭包
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
第⼆种就是使⽤ setTimeout 的第三个参数
for ( var i=1; i<=5; i++) {
setTimeout( function timer(j) {
console.log( j );
}, i*1000, i);
}
第三种就是使⽤ let 定义 i 了
for ( let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
因为对于 let 来说,他会创建⼀个块级作⽤域,相当于
{ // 形成块级作⽤域
let i = 0
{
let ii = i
setTimeout( function timer() {
console.log( ii );
}, i*1000 );
}
i++
{
let ii = i
}
i++
{
let ii = i
}
...
}
深浅拷贝
let a = {
age: 1 }
let b = a a.age = 2
console.log(b.age) // 2
从上述例⼦中我们可以发现,如果给⼀个变量赋值⼀个对象,那么两者的值会是同⼀个引
⽤,其中⼀⽅改变,另⼀⽅也会相应改变。
通常在开发中我们不希望出现这样的问题,我们可以使⽤浅拷⻉来解决这个问题。
浅拷贝
⾸先可以通过 Object.assign 来解决这个问题。
let a = {
age: 1 }
let b = Object.assign({}, a) a.age = 2
console.log(b.age) // 1
当然我们也可以通过展开运算符(…)来解决
let a = {
age: 1 }
let b = {...a} a.age = 2
console.log(b.age) // 1
通常浅拷⻉就能解决⼤部分问题了,但是当我们遇到如下情况就需要使⽤到深拷⻉了
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = {...a} a.jobs.first = 'native'
console.log(b.jobs.first) // native
浅拷⻉只解决了第⼀层的问题,如果接下去的值中还有对象的话,那么就⼜回到刚开始的话
题了,两者享有相同的引⽤。要解决这个问题,我们需要引⼊深拷⻉。
深拷贝
这个问题通常可以通过 JSON.parse(JSON.stringify(object)) 来解决。
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
但是该⽅法也是有局限性的:
会忽略 undefined
会忽略 symbol
不能序列化函数
不能解决循环引⽤的对象
let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)
如果你有这么⼀个循环引⽤对象,你会发现你不能通过该⽅法深拷⻉

在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化
let a = {
age: undefined,
sex: Symbol('male'),
jobs: function() {},
name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}
你会发现在上述情况中,该⽅法会忽略掉函数和 undefined 。
但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决⼤部分问题,并且
该函数是内置函数中处理深拷⻉性能最快的。当然如果你的数据中含有以上三种情况下,可
以使⽤ lodash 的深拷⻉函数。
如果你所需拷⻉的对象含有内置类型并且不包含函数,可以使⽤ MessageChannel
function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}
var obj = {a: 1, b: {
c: b
}}
// 注意该⽅法是异步的
// 可以处理 undefined 和循环引⽤对象
(async () => {
const clone = await structuralClone(obj)
})()