深入JavaScript(2)变量对象、作用域链、this;执行上下文的三个重要属性

114 阅读15分钟

1.变量对象

1.1基础

当 JavaScript 代码执⾏⼀段可执⾏代码(executable code)时,会创建对应的执⾏上下⽂(execution context)。

对于每个执⾏上下⽂,都有三个重要属性:

变量对象( Variable object ,VO);

作⽤域链( Scope chain );

this

这⾥着重讲变量对象

1.2变量对象

变量对象是与执⾏上下⽂相关的数据作⽤域,存储了在上下⽂中定义的变量和函数声明。

因为不同执⾏上下⽂下的变量对象稍有不同,所以我们来聊聊全局上下⽂下的变量对象和函数上下⽂下 的变量对象。

1.3全局上下文

  1. 全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使⽤全局对象,可以访问所有其他所有预定义的对象、函数和属性。

  2. 在顶层 JavaScript 代码中,可以⽤关键字 this 引⽤全局对象。因为全局对象是作⽤域链的头,这意味着所有⾮限定性的变量和函数名都会作为该对象的属性来查询。

  3. 例如,当JavaScript 代码引⽤ parseInt() 函数时,它引⽤的是全局对象的 parseInt 属性。全局对象是作⽤域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。

简单点说:

1.可以通过 this 引⽤,在客户端 JavaScript 中,全局对象就是 Window 对象。

console.log(this === window); //true
  1. 全局对象是由 Object 构造函数实例化的⼀个对象。
console.log(this instanceof Object); //true
  1. 预定义的属性是否可⽤
console.log(Math.random());
console.log(this.Math.random());

console.log(Math === this.Math); //true
  1. 作为全局变量的宿主
var a = 1;

console.log(this.a === a); //true
  1. 客户端 JavaScript 中,全局对象有 window 属性指向⾃身
this.window.b = 2
console.log(b) //2
console.log(this.b) //2

综上,对JS⽽⾔,全局上下⽂中的变量对象就是全局对象。

1.4函数上下⽂

在函数上下⽂中,我们⽤活动对象( activation object , AO)来表示变量对象。

活动对象和变量对象其实是⼀个东⻄,只是变量对象是规范上的或者说是引擎实现上的,不可在JavaScript 环境中访问,只有到当进⼊⼀个执⾏上下⽂中,这个执⾏上下⽂的变量对象才会被激活,所以才叫 activation object,⽽只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

活动对象是在进⼊函数上下⽂时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。

1.5执行过程

执⾏上下⽂的代码会分成两个阶段进⾏处理:分析和执⾏,我们也可以叫做:

  1. 进⼊执⾏上下⽂;
  2. 代码执⾏;

1.5.1进入执行上下文

当进⼊执⾏上下⽂时,这时候还没有执⾏代码,

变量对象会包括:

  1. 函数的所有形参 (如果是函数上下⽂)
    • 由名称和对应值组成的⼀个变量对象的属性被创建;
    • 没有实参,属性值设为 undefined;
  1. 函数声明
    • 由名称和对应值(函数对象(function-object))组成⼀个变量对象的属性被创建;
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性;
  1. 变量声明
    • 由名称和对应值(undefined)组成⼀个变量对象的属性被创建;
    • 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会⼲扰已经存在的这类属性;

举个例子:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function () {};
  b = 3;
}
foo(1);

在进⼊执⾏上下⽂后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}

1.5.2 代码执行

在代码执⾏阶段,会顺序执⾏代码,根据代码,修改变量对象的值;

还是上⾯的例⼦,当代码执⾏完后,这时候的 AO 是:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

到这⾥变量对象的创建过程就介绍完了,让我们简洁的总结我们上述所说:

  1. 全局上下⽂的变量对象初始化是全局对象;

  2. 函数上下⽂的变量对象初始化只包括 Arguments 对象;

  3. 在进⼊执⾏上下⽂时会给变量对象添加形参、函数声明、变量声明等初始的属性值;

  4. 在代码执⾏阶段,会再次修改变量对象的属性值;

1.5.3 例子

function foo() {
  console.log(a);
  a = 1;
}
foo(); // ???

function bar() {
  a = 1;
  console.log(a);
}
bar(); // ???

第⼀段会报错: Uncaught ReferenceError: a is not defined 。

第⼆段会打印:1。

这是因为函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。

第⼀段执⾏ console 的时候, AO 的值是:

AO = {
  arguments: {
    length: 0,
  },
};

没有 a 的值,然后就会到全局去找,全局也没有,所以会报错。

当第⼆段执⾏ console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印 1。

例子2

console.log(foo);
function foo(){
 console.log("foo");
}
var foo = 1;

会打印函数,⽽不是 undefined 。

这是因为在进⼊执⾏上下⽂时,⾸先会处理函数声明,其次会处理变量声明,如果变量名称跟已经声明 的形式参数或函数相同,则变量声明不会⼲扰已经存在的这类属性。

但是在代码执行阶段,变量foo 就会覆盖 函数foo。

console.log(foo); // ƒ foo(){ console.log("foo"); }
function foo(){
 console.log("foo");
}
var foo = 1;
console.log(foo); //1

2.作用域链

上文讲到,当JavaScript代码执⾏⼀段可执⾏代码( executable code )时,会创建对应的执⾏上下⽂ ( execution context )。

对于每个执行上下文,对于每个执⾏上下⽂,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作⽤域链(Scope chain)
  • this

本节讲作⽤域链。

2.1作用域链

上节讲到,当查找变量的时候,会先从当前上下⽂的变量对象中查找,如果没有找到,就会从⽗级(词法层⾯上的⽗级)执⾏上下⽂的变量对象中查找,⼀直找到全局上下⽂的变量对象,也就是全局对象。这样由多个执⾏上下⽂的变量对象构成的链表就叫做作⽤域链。

2.2函数创建

上文的词法作用域讲到,函数的作用域在函数定义的时候就决定了。

这是因为函数有⼀个内部属性 [[scope]],当函数创建的时候,就会保存所有⽗变量对象到其中,你可以理解 [[scope]] 就是所有⽗变量对象的层级链,但是注意:[[scope]] 并不代表完整的作⽤域链!

example:

function foo() {
    function bar() {}
}

函数创建时,各自的[[scope]]为:

foo.[[scope]] = [
    globalContext.VO
]
bar.[[scope]] = [
    fooContext.AO,
    globalContext.VO
]

2.3函数激活

当函数激活时,进⼊函数上下⽂,创建 VO/AO 后,就会将活动对象添加到作⽤链的前端。这时候执⾏上下⽂的作⽤域链,我们命名为 Scope:

Scope = [AO].concat([[scope]])

⾄此,作⽤域链创建完毕。

2.4总结

结合着之前讲的变量对象和执⾏上下⽂栈,我们来总结⼀下函数执⾏上下⽂中作⽤域链和变量对象的创建过程:

var scope = "global scope";
function checkscope() {
    var scope2 = 'local scope';
    return scope2
}
checkscope();

执⾏过程如下:

1.checkscope 函数被创建,保存作⽤域链到 内部属性[[scope]]

checkscope.[[scope]] = [
    globalContext.VO
];

2.执⾏ checkscope 函数,创建 checkscope 函数执⾏上下⽂,checkscope 函数执⾏上下⽂被压⼊执⾏上下⽂栈

ECStack = [
    checkscopeContext,
    globalContext
];

3.checkscope 函数并不⽴刻执⾏,开始做准备⼯作,第⼀步:复制函数[[scope]]属性创建作⽤域链

checkscopeContext = {
    Scope: checkscope.[[scope]]
}

4.第⼆步:⽤ arguments 创建活动对象,随后初始化活动对象,加⼊形参、函数声明、变量声明

checkscopeContext = {
    AO: {
        arguments:{
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]]
}

5.第三步:将活动对象压⼊ checkscope 作⽤域链顶端

checkscopeContext = {
    AO: {
        arguments:{
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[scope]]]
}

6.准备⼯作做完,开始执⾏函数,随着函数的执⾏,修改 AO 的属性值

checkscopeContext = {
    AO: {
        arguments:{
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[scope]]]
}

7.查找到 scope2 的值,返回后函数执⾏完毕,函数上下⽂从执⾏上下⽂栈中弹出

ECStack = [
    globalContext
];

3.this

对于每个执行上下文,对于每个执⾏上下⽂,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作⽤域链(Scope chain)
  • this

本节讲this

借鉴 ECMASciript5

ECMAScript 5.1 规范地址:

英文版:es5.github.io

中文版:yanhaijing.com/es5/#about

3.1Types

ECMASciript5 第 8 章 Types:

Types are further subclassified into ECMAScript language types and specification types.

An ECMAScript language type corresponds to values that are directly manipulated by an ECMAScript programmer using the ECMAScript language. The ECMAScript language types are Undefined, Null, Boolean, String, Number, and Object.

A specification type corresponds to meta-values that are used within algorithms to describe the semantics of ECMAScript language constructs and ECMAScript language types. The specification types are Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, and Environment Record.

简单的翻译一下:

ECMAScript 的类型分为语言类型和规范类型。

ECMAScript 语言类型是开发者直接使用 ECMAScript 可以操作的。其实就是我们常说的Undefined, Null, Boolean, String, Number, 和 Object。

而规范类型相当于 meta-values,是用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型的。规范类型包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。

我们只要知道在 ECMAScript 规范中还有⼀种只存在于规范中的类型,它们的作⽤是⽤来描述语⾔底层⾏为逻辑。

(简单理解一下,规范类型就是提供给我们使用语言类型的能力)

这⾥要讲的重点是便是其中的 Reference 类型。它与 this 的指向有着密切的关联。

3.2Reference

那什么又是 Reference ?

ECMASciript5 8.7 章 The Reference Specification Type:

The Reference type is used to explain the behaviour of such operators as delete, typeof, and the assignment operators.

Reference 类型用于解释 delete、typeof 和赋值运算符等运算符的行为。

抄袭尤雨溪大大的话,就是:

这里的 Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。

再看看接下来的这段具体介绍 Reference 的内容:

A Reference is a resolved name binding.

A Reference consists of three components, the base value, the referenced name and the Boolean valued strict reference flag.

The base value is either undefined, an Object, a Boolean, a String, a Number, or an environment record (10.2.1).

A base value of undefined indicates that the reference could not be resolved to a binding. The referenced name is a String.

这段讲述了 Reference 的构成,由三个组成部分,分别是:

  • base value

  • referenced name

  • strict reference

我们简单的理解的话:

base value 就是当前属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种。

referenced name 就是属性的名称。

举个例子:

var foo = 1;
 
// 对应的Reference是:
var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};

再举个例子:

var foo = {
    bar: function () {
        return this;
    }
};
 
foo.bar(); // foo
 
// bar对应的Reference是:
var BarReference = {
    base: foo,
    propertyName: 'bar',
    strict: false
};

而且规范中还提供了获取 Reference 组成部分的方法,比如 GetBase 和 IsPropertyReference。

3.2.1GetBase

GetBase(V). Returns the base value component of the reference V.

返回 reference 的 base value。

var foo = 1;
 
var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};
 
GetValue(fooReference) // 1;

GetValue 返回对象属性真正的值,但是!

调⽤ GetValue,返回的将是具体的值,⽽不再是⼀个Reference

3.2.2IsPropertyReference

IsPropertyReference(V). Returns true if either the base value is an object or HasPrimitiveBase(V) is true; otherwise returns false.

简单理解:如果 base value 是⼀个对象,就返回true。

3.3如何确定this的值

关于 Reference 讲了那么多,为什么要讲 Reference 呢?到底 Reference 跟本文的主题 this 有哪些关联呢?如果你能耐心看完之前的内容,以下开始进入高能阶段:

于 ECMASciript5 11.2.3 Function Calls:

这里讲了当函数调用的时候,如何确定 this 的取值。

只看第一步、第六步、第七步:

1.Let ref be the result of evaluating MemberExpression.

6.If Type(ref) is Reference, then

a.If IsPropertyReference(ref) is true, then

i.Let thisValue be GetBase(ref).

b.Else, the base of ref is an Environment Record

i.Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).

7.Else, Type(ref) is not Reference.

a. Let thisValue be undefined.

让我们描述一下:

1.计算 MemberExpression 的结果赋值给 ref

2.判断 ref 是不是一个 Reference 类型

2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)

2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)

2.3 如果 ref 不是 Reference,那么 this 的值为 undefined

3.4具体分析

3.4.1计算 MemberExpression 的结果赋值给 ref

什么是 MemberExpression?看规范 11.2 Left-Hand-Side Expressions:

MemberExpression :

  • PrimaryExpression // 原始表达式 可以参见《JavaScript权威指南第四章》

  • FunctionExpression // 函数定义表达式

  • MemberExpression [ Expression ] // 属性访问表达式

  • MemberExpression . IdentifierName // 属性访问表达式

  • new MemberExpression Arguments // 对象创建表达式

举个例子:

function foo() {
    console.log(this)
}
 
foo(); // MemberExpression 是 foo
 
function foo() {
    return function() {
        console.log(this)
    }
}
 
foo()(); // MemberExpression 是 foo()
 
var foo = {
    bar: function () {
        return this;
    }
}
 
foo.bar(); // MemberExpression 是 foo.bar

所以简单理解 MemberExpression 其实就是()左边的部分。

3.4.2判断 ref 是不是一个 Reference 类型。

关键就在于看规范是如何处理各种 MemberExpression,返回的结果是不是一个Reference类型。

var value = 1;
 
var foo = {
  value: 2,
  bar: function () {
    return this.value;
  }
}
 
//示例1
console.log(foo.bar()); // 2
//示例2
console.log((foo.bar)()); // 2
//示例3
console.log((foo.bar = foo.bar)()); // 1
//示例4
console.log((false || foo.bar)()); // 1
//示例5
console.log((foo.bar, foo.bar)()); // 1

示例1 foo.bar()

在示例 1 中,MemberExpression 计算的结果是 foo.bar,那么 foo.bar 是不是一个 Reference 呢?

查看规范 11.2.1 Property Accessors,这里展示了一个计算的过程,什么都不管了,就看最后一步:

Return a value of type Reference whose base value is baseValue and whose referenced name is propertyNameString, and whose strict mode flag is strict.

我们得知该表达式返回了一个 Reference 类型!

根据之前的内容,我们知道该值为:

var Reference = {
  base: foo,
  name: 'bar',
  strict: false
};

看规范 11.2.3 Function Calls,第六步:

如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)

该值是 Reference 类型,那么 IsPropertyReference(ref) 的结果是多少呢?

前面我们已经铺垫了 IsPropertyReference 方法,如果 base value 是一个对象,结果返回 true。

base value 为 foo,是一个对象,所以 IsPropertyReference(ref) 结果为 true。

这个时候我们就可以确定 this 的值了:

this = GetBase(ref),

GetBase 也已经铺垫了,获得 base value 值,这个例子中就是foo,所以 this 的值就是 foo ,示例1的结果就是 2!

示例2 (foo.bar)()

foo.bar 被 () 包住,查看规范 11.1.6 The Grouping Operator

直接看结果部分:

Return the result of evaluating Expression. This may be of type Reference.

NOTE This algorithm does not apply GetValue to the result of evaluating Expression.

实际上 () 并没有对 MemberExpression 进行计算,所以其实跟示例 1 的结果是一样的。

示例3 (foo.bar = foo.bar)()

有赋值操作符,查看规范 11.13.1 Simple Assignment ( = ):

计算的第三步:

3.Let rval be GetValue(ref).

因为使用了 GetValue,所以返回的值不是 Reference 类型,

按照之前讲的判断逻辑:

2.3 如果 ref 不是Reference,那么 this 的值为 undefined

this 为 undefined,非严格模式下,this 的值为 undefined 的时候,其值会被隐式转换为全局对象。

示例4 (false || foo.bar)()

逻辑或算法,查看规范 11.11 Binary Logical Operators:

计算第二步:

2.Let lval be GetValue(lref).

因为使用了 GetValue,所以返回的不是 Reference 类型,this 为 undefined

示例5 (foo.bar, foo.bar)()

逗号操作符,查看规范11.14 Comma Operator ( , )

计算第二步:

2.Call GetValue(lref).

因为使用了 GetValue,所以返回的不是 Reference 类型,this 为 undefined

注意:以上是在非严格模式下的结果,严格模式下因为 this 返回 undefined,所以示例 3 会报错。

补充

function foo() {
    console.log(this)
}
 
foo(); 

MemberExpression 是 foo,解析标识符,查看规范 10.3.1 Identifier Resolution,会返回一个 Reference 类型的值:

var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};

接下来进行判断:

2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)

因为 base value 是 EnvironmentRecord,并不是一个 Object 类型,还记得前面讲过的 base value 的取值可能吗?只可能是 undefined, an Object, a Boolean, a String, a Number, 和 an environment record 中的一种。

IsPropertyReference(ref) 的结果为 false,进入下个判断:

2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)

base value 正是 Environment Record,所以会调用 ImplicitThisValue(ref)

查看规范 10.2.1.1.6,ImplicitThisValue 方法的介绍:该函数始终返回 undefined。

所以最后 this 的值就是 undefined。

追根溯源的从 ECMASciript 规范看待 this 的指向

4.执行上下文

4.1 思考题

var scope = "global scope";
function checkscope() {
  var scope = "local scope";
  function f() {
    return scope;
  }
  return f();
}
checkscope();
var scope = "global scope";
function checkscope() {
  var scope = "local scope";
  function f() {
    return scope;
  }
  return f;
}
checkscope()();

两段代码都会打印'local scope',在上⽂讲到了两者的区别在于执⾏上下⽂栈的变化不⼀样,本节会在此基础上,详细的解析执⾏上下⽂栈和执⾏上下⽂的具体变化过程。

4.2 具体执行分析

执行过程如下:

1.执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈

ECStack = [
        globalContext
    ];

2.全局上下文初始化

globalContext = {
        VO: [global, scope, checkscope],
        Scope: [globalContext.VO],
        this: globalContext.VO
    }

3.初始化的同时,checkscope函数被创建,保存作用域链到[[scope]]

checkscope.[[scope]] = [
      globalContext.VO
    ];

4.执行checkscope函数,创建checkscope函数执行上下文,checkscope函数执行上下文被压入执行上下文栈

ECStack = [
        checkscopeContext,
        globalContext
    ];

5.checkscope函数执行上下文初始化:

  1. 复制函数[[scope]]属性创建作用域链,
  2. 用arguments创建活动对象,
  3. 初始化活动对象,即加入形参、函数声明、变量声明,
  4. 将活动对象压入checkscope作用域链顶端。

同时f函数被创建,保存作用域链到[[scope]]

checkscopeContext = {
        AO: {
            arguments: {
                length: 0
            },
            scope: undefined,
            f: reference to function f(){}
        },
        Scope: [AO, globalContext.VO],
        this: undefined
    }

6.执行f函数,创建f函数执行上下文,f函数执行上下文被压入执行上下文栈

ECStack = [
        fContext,
        checkscopeContext,
        globalContext
    ];

7.f函数执行上下文初始化, 以下跟第4步相同:

  1. 复制函数[[scope]]属性创建作用域链
  2. 用arguments创建活动对象
  3. 初始化活动对象,即加入形参、函数声明、变量声明
  4. 将活动对象压入f作用域链顶端
fContext = {
        AO: {
            arguments: {
                length: 0
            }
        },
        Scope: [AO, checkscopeContext.AO, globalContext.VO],
        this: undefined
    }

8.f函数执行,沿着作用域链查找scope值,返回scope值

9.f函数执行完毕,f函数上下文从执行上下文栈中弹出

ECStack = [
        checkscopeContext,
        globalContext
    ];

10.checkscope函数执行完毕,checkscope执行上下文从执行上下文栈中弹出

ECStack = [
        globalContext
    ];