跳到主要内容

作用域和执行上下文

作用域

作用域用于指定 JS 查找变量的区域,在 chrome 调试面板的 Scope 中可以看到相关信息,JS 中包括以下几个作用域:

  • 全局作用域
  • 函数作用域
  • ES6 module 引入的模块作用域
  • let const 引入的块作用域

作用域也同样呈现链状,变量查找时依次向上查找直至全局作用域。

静态作用域和动态作用域

JS 为静态作用域/词法作用域,在进行词法解析时就已经确定了作用域的范围完成作用域创建。

相对而言动态作用域则是函数调用时才创建,如 bash 中即为动态作用域。

value='outer'
function foo () {
echo $value;
}
function bar () {
local value='inner';
foo;
}
bar

因为 foo 在 bar 中调用,所以会打印 inner,而直接 foo 则会打印 outer

执行上下文

  • 执行上下文一般为栈状结构

  • 代码执行时首先会执行全局代码,所以会先推入一个全局执行上下文。

  • 函数执行时会创建一个函数执行上下文,并推入执行上下文栈中

  • 当函数执行完成对应的函数执行上下文会被弹出

  • 程序中所有代码都执行完成后全局执行上下文会被弹出

  • 在 chrome 调试面板右侧 Call stack 中可以看到执行上下文调用栈

  • 递归次数多时因为每次递归前一次执行上下文都无法弹出会导致一直向调用栈中推入新的上下文,导致爆栈

  • 执行上下文信息中包含了变量对象(Variable Object 即 VO)、作用域链和 this

  • 变量对象中保存着上下文中定义的变量、函数、当前执行函数的形参等

  • 全局执行上下文

闭包

闭包只是对函数作用域的一种特殊使用方式,因为 JS 的静态作用域让函数外部无法访问到内部的变量,而在函数内部定义的函数则可以访问,所以借助此特性可以在函数 执行时将内部函数作为返回值,即可让外部借由返回值操控内部的变量,保障了变量的修改途径。

深入讲内部函数在定义时作用域就固定了,将它返回并保存时,它的作用域链也会被保存在内存中,无法释放,所以随便这个返回的函数在何处调用,它的作用域链都不会 变化,都可以控制它定义时的外部函数中的变量。

this 1

this 的指向和他所在的函数体相关,下面称为 this 函数体:

  • this 函数体如果为箭头函数,则 this 指向该箭头函数所定义时的 this 上下文。
  • this 函数体被调用时如果为 call、apply、bind 等间接调用方式,则指向对应的方式所指定的 this 上下文。
  • this 函数体被调用时如果是被浏览器事件等调用,则指向该事件所绑定的元素。
  • this 函数体如果作为方法被调用则指向该方法所属的对象。
  • 否则 this 指向 window/global 或 undefined。

所以除了箭头函数中的 this 上下文和函数体定义时的上下文相关,其它的情况直接看 this 函数体时如何被调用的即可判定。

call、apply、bind

call 和 apply 相同,都是修改函数执行时的 this 指向,只是接收的参数形式不同。

fn.call(ctx, ...args);
fn.apply(ctx, args);

一般使用场景为修改方法的调用对象或者用作实现继承。

bind 用于创建一个 this 被指定的函数,且可以提前指定部分参数。

function fn(...args) {
console.log(this, ...args);
}
const fn1 = fn.bind({ a: 1 });
const fn2 = fn.bind({ a: 1 }, 1, 2, 3);
fn1('a', 'b', 'c');
// {a: 1} 'a' 'b' 'c'
fn2('a', 'b', 'c');
// {a: 1} 1 2 3 'a' 'b' 'c'