前端面试题(三)执行上下文 & 闭包 & 深浅拷贝

本文详细介绍了JavaScript的执行上下文,包括全局、函数及eval上下文,强调了变量对象、作用域链和this的概念。文章还讨论了变量提升、立即执行函数表达式以及闭包的工作原理,特别是如何利用闭包解决变量作用域问题。同时,提到了深浅拷贝的区别和应用场景,并给出了解决循环引用和序列化问题的策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

执行上下文

当执⾏ 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)
})()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值