备战26春招,彻底搞懂前端八股之作用域:知识点讲解+面试示例回答

在 JavaScript 中,作用域是一个非常基础、非常核心,但又让很多同学容易混淆的知识点。面试时大家往往说不清楚「作用域在哪里生效」「作用域链怎么查找」「var、let、const 的区别」「闭包为什么能访问外部变量」这些关键点。

本文将带你彻底理解作用域、作用域链、声明提升、闭包等相关概念,并附带面试中一口气说清楚、让面试官满意的示范回答。

知识点讲解

作用域

在 JS 中,作用域规定了变量可以被访问的范围。

一个变量只能在它的作用域内被访问,如果当前不在该变量的作用域内,就访问不到。

例如:

function func() {
  const a = 10;
}
console.log(a); // 报错:a 不在此作用域

函数内部声明的变量,只能在当前函数中访问。

作用域嵌套与作用域链

当一个函数或代码块嵌套在另一层内部时,就产生了作用域的“嵌套关系”。

当 JS 引擎要查找某个变量时:

  1. 先在当前作用域查找
  2. 找不到就去外层作用域
  3. 再找不到就继续向外层找,一直找到全局作用域
  4. 全局还找不到就报错

例如:

const x = 1;

function outer() {
  const y = 2;

  function inner() {
    const z = 3;
    console.log(x, y, z);
  }

  inner();
}
outer();

查找顺序:

inner → outer → global → 找到为止

这条从内到外逐级查找的路径,就是作用域链。

遮蔽效应(变量遮蔽)

如果内层作用域定义了与外层作用域相同的变量名,那么内层会把外层“遮住”。

let a = 1;

function test() {
  let a = 2;
  console.log(a); // 2(内层覆盖外层)
}

test();

作用域链查找时遇到最近的变量就会停止。

全局作用域中用 var 会自动挂到 window 上

var a = 1;
console.log(window.a); // 1

let b = 2;
console.log(window.b); // undefined

只有 varfunction 声明 会被挂载到全局对象。

作用域只和函数的“声明位置”有关,与调用位置无关

var a = 1;

function foo() {
  var a = 2
  console.log(a);
}

function bar() {
  var a = 3;
  foo(); // 输出 2
}

bar();

当我们在 bar 内部调用 foo 时,foo 内部查找 a 时,仍然是沿着 foo 内部 -> 全局作用域 这样的作用域链进行查找,不会找到 bar 内部的 a,即使 foo 是在 bar 内部调用的。这是因为,作用域只和函数的“声明位置”有关,与调用位置无关

作用域的意义之一:私有变量

把变量放在函数内部,就形成私有变量,外部无法直接访问。

function counter() {
  let count = 0;
  return function () {
    count++;
    return count;
  };
}

const add = counter();
console.log(add()); // 1
console.log(add()); // 2

console.log(count); // 报错,因为count 不在当前作用域内

私有变量可以防止全局变量污染,也避免意外修改。

作用域的意义之二:避免变量冲突

function f1() {
  const a = 10;
  console.log(a) // 10
}

function f2() {
  const a = 20;
  console.log(a) // 20
}

两个函数中可以访问各自的 a,互相不会冲突、覆盖。

如果都写全局:

var a = 10;
var a = 20;
console.log(a); // 20,被覆盖

此时第二个 a 会覆盖第一个 a

JS 中三种作用域

JS中,有全局作用域、函数作用域、块级作用域。

在全局下使用 var 、let、const 声明变量,这个变量的作用域是全局

在函数内使用 var 声明变量,这个变量的作用域是当前函数内部

在 for、if 这些由大括号包裹的代码块内使用 let、const 声明变量,这个变量的作用域是当前块内部

假设我们在 for 循环中使用 var 声明一个 i 变量,那么这个变量的作用域是全局,而不是 for 内部。这个行为是有风险的。

可以看以下的代码:

if (true) {
  let x = 1;   // 块级作用域
}
console.log(x); // 报错

注意:

for (var i = 0; i < 3; i++) {}
console.log(i); // 3(var 没有块级作用域)

声明提升

函数声明和变量声明,会提升到当前作用域的顶端。在JS中,var a = 2 这样的代码,会被引擎看作两个步骤,一个是 var a 这个声明步骤,一个是 a = 2 这个赋值步骤。只有 var a 这个声明过程会被提升,而 a=2这个赋值过程不会被提升。如果同时有函数提升和变量提升,则函数提升优先于变量提升去执行。提升有什么用呢?假设我们在第五行声明一个函数,而在第三行去执行这个函数,是可以正常执行的,而不会报错;假设我们在第六行 var a = 2,在第二行访问 a,此时会得到 undefined,而不会报错。

console.log(a); // undefined
var a = 2;

引擎内部实际处理成:

var a;
console.log(a); // undefined
a = 2;

函数提升优先于变量提升,<font style="color:rgb(10, 10, 10);">且不会被变量提升覆盖</font>:

console.log(a); // function a()

function a() {}
var a = 2;

let / const 存在暂时性死区

还有一个要注意的点,let、const 声明的变量,会存在暂时性死区,暂时性死区的意思是,假设我们在 第七行 let b = 3,在第一行访问 b,此时会报错

console.log(b); // 报错
let b = 3;

闭包(Closures)

还有一个跟作用域有关的概念,叫做闭包。

当内层函数 访问了外层作用域中的变量,就形成了闭包。

闭闭包的作用有两个,首先是它让我们可以把这个变量作为外部函数的私有化变量,让内层函数只能按照我们规定的方式访问;另一个作用是,可以延长这个变量的生命周期。本来这个变量会随着外层函数的执行完成而被垃圾回收机制回收,但是现在由于它被内层函数访问,所以只要内层函数还存在,那么这个变量就不会被回收。

可以看下面这个例子:

function create() {
  let value = 0;

  return function () {
    return ++value;
  };
}

const add = create();
add(); // 1
add(); // 2

因为内层函数引用了 value,所以外层执行结束后 value 仍然保留。

闭包在JS中有很多具体的应用场景,比如函数柯里化、防抖截流,甚至于 react 的 hooks,都用到了闭包的原理。

但是由于变量不会被及时回收,闭包也有一个隐患,就是可能会导致内存泄漏。所以我们需要在使用完毕之后,即使地把变量赋值为 null,以便垃圾回收机制回收

以上就是跟作用域相关的全部概念了。接下来我们来看下面试中应当如何回答,以提升竞争力:

面试回答示例

面试官问:你如何理解作用域和作用域链?

示范回答:

作用域决定了变量能够被访问的范围。在 JavaScript 中主要有三种作用域:全局作用域、函数作用域和块级作用域。 var 声明的变量是函数作用域,而 let 和 const 声明的变量是块级作用域。

当一个作用域嵌套在另一个作用域内部时,就形成了作用域嵌套。当我们访问一个变量时,JS 引擎会先在当前作用域查找,如果找不到,就向外层作用域查找,直到找到该变量或到达全局作用域为止。这就是作用域链

如果内层作用域声明了与外层同名的变量,会产生遮蔽效应,也就是内层变量会覆盖外层变量。

作用域的一个重要意义是变量隔离和避免变量冲突;另一个意义是实现变量私有化,例如把变量写在函数内部,避免被全局访问到。

var 和函数声明会产生声明提升,但是变量提升的话只有 声明过程会被提升,赋值过程不会被提升。这意味着在函数声明之前、变量声明之前,我们是能访问到他们的。但是 let / const 存在暂时性死区,在声明之前不能访问。

跟作用域有关的另一个概念是闭包。当一个函数访问了外部函数作用域中的变量时,就产生了闭包。闭包会让外层变量不会立即被销毁,所以可以利用闭包来实现变量的私有化和持久化,例如函数柯里化、节流防抖、React Hooks 内部逻辑都基于闭包。不过闭包有一个隐患,它可能导致内存泄漏,所以我们在开发中需要注意在适当时机释放。

前端新一水八股系列 文章被收录于专栏

前端新一水八股系列,每日讲解一道面试高频八股题,涵盖CSS、JS、Vue、React、工程化、性能优化、场景题等

全部评论

相关推荐

不愿透露姓名的神秘牛友
10-31 14:37
点赞 评论 收藏
分享
10-28 10:48
已编辑
门头沟学院 Java
孩子我想要offer:发笔试后还没笔试把我挂了,然后邮箱一直让我测评没测,后面不知道干嘛又给我捞起来下轮笔试,做完测评笔试又挂了😅
点赞 评论 收藏
分享
评论
1
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务