JS入门理解——预编译、作用域、词法作用域和作用域链

178 阅读6分钟

一、预编译

预编译发生在代码执行之前,可以分为全局预编译和局部预编译。

(1)全局预编译

全局上的预编译,发生在全局。它分为以下三个步骤(三部曲):

  1. 创建 GO 对象 (Global Object)
  2. 找全局的变量声明,将变量声明作为 GO 的属性名,值为undefined
  3. 在全局找函数声明,将函数名作为 GO 对象的属性名,值赋予函数体

示例

var global = 100
function fn(){
    console.log(global)
    global = 200
    console.log(global)
}
fn()

在这段代码执行之前,它首先进行了全局预编译,创建了 GO 对象,第二步在全局中找到了变量声明 var global ,因此 global 将作为 GO 对象的属性名,值被赋为 undefined ,第三步在全局中找到了函数声明 function fn ( ) { } ,因此函数名 fn 也将作为 GO 对象的属性名,值被赋为 fn ( ) { } 。

全局预编译结束后,就是从上往下执行了, global 被赋值为100,然后执行函数 fn ( ) ,(注:在执行函数 fn ( ) 前,要对该函数进行局部预编译,在这里该函数预编译的结果为空,不做过多赘述,详细原因请看本文 局部预编译 ),最后的输出结果为100以及200。下面是 GO 对象中的赋值过程:

GO {
    global: undefined -> 100 -> 200,
    fn: function fn (){}
}

(2)局部预编译

函数体上的预编译,发生在函数执行之前。它分为以下四个步骤(四部曲):

  1. 创建AO对象 (Activation Object)
  2. 找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined
  3. 将实参和形参统一
  4. 在函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体

示例

function fn(a){
    console.log(a) // 输出结果为:function a(){}
    var a = 123
    console.log(a) // 输出结果为:123
    function a(){}
    console.log(a) // 输出结果为:123
    var b = function(){}
    console.log(b) // 输出结果为:function(){}
    function d(){}
    var d = a
    console.log(a) // 输出结果为:123
    console.log(d) // 输出结果为:123
}
fn(1)

局部预编译和全局预编译大体一致,如果函数有形参,只要将实参和形参统一就可以了,也就是把 fn(1) 中的1赋值给形参 a 。输出结果已经给到函数之中,下面是 AO 对象中的赋值过程:

AO: {
    a: undefined -> 1 -> function a(){} -> 123,
    b: undefined,
    d: undefined -> function d(){}
}

二、作用域

(1)什么是作用域

概念:作用域是负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。简单来说,作用域就是根据名称查找变量的一套规则。

(2)作用域的分类

1.全局作用域

在整个script标签或者一个js文件中的作用域就称为全局作用域,其中声明的变量在全局中都可以使用。

var a = 'global'
function foo () {
    console.log(a) // 输出结果为:global
}
foo()

2.局部作用域

在函数内部的作用域就称为局部作用域,也被称为函数作用域,其中声明的变量只能在这个函数中起作用。

function foo () {
    var a = 'global'
    console.log(a)
}
console.log(a) // 报错 ReferenceError:  a is not defined

3.块级作用域

ECMAScript 6 (简称ES6)中新增了块级作用域,块级作用域由 { } 包括,如if语句和for语句里面的 { } 都属于块级作用域。使用let或者const声明的变量只能在块级作用域里被访问。

for(var a = 0; a < 10; a++){
    console.log(a)
}
console.log(a) // 打印结果为:10var 声明的 a 会挂在外部作用域上

for(let b = 0; b < 10; b++){
    console.log(b) 
}
console.log(b)// 报错:ReferenceError: b is not defined,let 声明的变量 b 使其只能在块级作用域内被访问

三、词法作用域

(1)什么是词法作用域

代码在执行之前需要被编译,编译过程的第一个工作阶段就是词法化(也叫单词化),而词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块级作用域写在哪里来决定的。

(2)词法化

  • 父级作用域是不能取子集作用域的字符的,子集作用域是可以调用父级作用域的字符的
  • 执行阶段作用域的查找是由内到外的(找到了第一个就会停止查找 --- 遮蔽效应)
  • 全局变量会变成window对象上的属性

(3)欺骗词法作用域

eval() 函数可以接受一个字符串为参数,并将其内容视为本就书写在程序中这个位置的代码,让原本不属于这里的代码好像天生就写在了这里一样。但 eval() 函数在严格模式下无法改变作用域。

示例

function a (str){
    eval(str)
    console.log(b) // 打印 1
}
a('var b = 1')

// 严格模式下
function c (str){
    'use strict' // 让浏览器引擎进入严格模式
    eval(str)
    console.log(d) // 报错:ReferenceError: d is not defined
}
c('var d = 1')

with 关键字通常被当作重复引用同一个对象的多个属性的快捷方式,可以不需要重复引用对象本身。但是当使用with修改对象当中不存在的属性时,这个属性将会被泄露到全局中。

示例

// 批量修改
var obj = {
    a: 1,
    b: 2,
    c: 3
}
with(obj) {
    a = 3
    b = 4
    c = 5
} 
console.log(obj)

// 修改不存在的属性
function foo(obj){
    with(obj){
        a = 2
    }
}
var o1 = {
    b:3
}
foo(o1)
console.log(o1.a) // undefined
console.log(a) // 打印:2  a被泄露到了全局

注:欺骗词法作用域会导致性能下降

四、作用域链

(1)执行期上下文

当函数执行的时候,会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕,它所产生的执行期上下文会被销毁。同样,执行期上下文也可以分为全局上下文和局部上下文。

(2)[[scope]]

函数的作用域,是不可访问的,其中存储了执行期上下文。

(3)作用域链

[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式连接,我们把这种链式连接叫做作用域链。

(4)查找变量

从作用域链的顶端依次往下查找。

示例

function a(){
  function b(){
    var b = 222
  }
  var a = 111
  b()
  console.log(a);
}
var glob = 100
a()

// a 定义  a.[[scope]]  ---> 0: GO{}
// a 执行  a.[[scope]]  ---> 0: aAO{} 1: GO{}
// b 定义  b.[[scope]]  ---> 0: bAO{} 1: aAO{} 2:GO{}

首先 a 的定义为我们带来了 a.[[scope]] ,全局代码的执行为我们带来了 GO{ } ,此时a.[[scope]] ---> 0: GO{ } ,放在作用域链的顶端,随后 a 的执行带来了 aAO{ } ,aAO{ } 也被放在作用域链的顶端,而 GO{ } 则被挤到了第二个,得到 “ 0: aAO{ } 1: GO{ } ”。以此类推,最后我们得到了 “ 0: bAO{ }, 1: aAO{ }, 2:GO{ } ” 这样一条作用域链。

最后函数执行查找变量会从作用域链的顶端依次往下查找,当一个函数执行完毕,其对应的执行期上下文就会被销毁,这就解释了为什么父级作用域是不能访问子级作用域,而子级作用域可以访问父级作用域。

结语

这是本人在预编译、作用域、词法作用域和作用域链中的一点理解,到这里就结束了,如有理解不当的地方,还望斧正。