资讯专栏INFORMATION COLUMN

JS学习系列 06 - 变量对象

Stardustsky / 357人阅读

摘要:变量对象就是执行上下文和作用域链中间的桥梁。作用域链和留到后面再讲,今天我们先来弄明白变量对象。全局执行上下文环境全局对象全局上下文环境的变量对象引用全局对象自身属性作用域链举个例子因此,在全局上下文环境中,变量对象用全局对象来表示。

上一节我们讨论了执行上下文,那么在上下文中到底有什么内容,为什么它会和作用域链扯上关系,JS 解释器又是怎么找到我们声明的函数和变量,看完这一节,相信大家就不会再迷惑了。

变量对象就是执行上下文作用域链中间的桥梁。
剧透一下,神秘的 this 就存在于执行上下文环境之中!
当然,之后我会多带带用几节来彻底讲明白 this 到底是什么(其实 this 很简单)。

接下来,我们进入正文。

1. 执行上下文包含什么

一个执行上下文我们可以抽象的理解为对象(object)。
每一个执行上下文都有一些属性(又称为上下文状态),它们用来追踪关联代码的执行进度。

我用一个结构图来说明:

Variable Object 就代表变量对象。
Scope Chain 代表作用域链。
thisValue 代表神秘的 this 。

作用域链和 this 留到后面再讲,今天我们先来弄明白变量对象

2. 变量对象
A variable object is a scope of data related with the execution context. It’s a special object associated with the context and which stores variables and function declarations are being defined within the context.

变量对象(variable object) 是与执行上下文相关的数据作用域(scope of data) 。它是与上下文关联的特殊对象,用于存储被定义在上下文中的 变量(variables) 和 函数声明(function declarations) 。

变量对象(Variable Object -- 简写 VO)是一个抽象的概念,指代与执行上下文相关的特殊对象,它存储着在上下文中声明的:

变量(var)

函数声明 (function declaration,简写 FD)

函数的形参(arguments)

我们假设变量对象为一个普通 ECMAScript 对象:

VO = {};

就像前面讲过的,VO 是执行上下文的一个属性:

activeExecutionContext = {
  VO: {
    // 上下文数据 (vars, FD, arguments)
  }
}

因为变量对象是一个抽象的概念,所以并不能通过变量对象的名称直接访问,但是却可以通过别的方法来间接访问变量对象,例如在全局上下文环境的变量对象会有一个属性 window (DOM 中) 可以引用变量对象自身,全局上下文环境的另一个属性 this 也指向全局上下文环境的变量对象。

举个例子:

var a = 2;

function foo (num) {
   var b = 5;
}

(function exp () {
   console.log(111);
})

foo(10);

这里对应的变量对象是:

// 全局上下文环境的变量对象
VO(globalContext) = {
   // 一些全局环境初始化时系统自动创建的属性: Math、String、Date、parseInt等等
   ···

   // 全局上下文的变量对象中有一个属性可以访问到自身,在浏览器中这个属性是 window ,在 node 中这个属性是 global
   window: global

   // 自己定义的属性
   a: 10,
   foo: 
};

// foo 函数上下文的变量对象
VO(foo functionContext) = {
   num: 10,
   b: 5
};

注意:函数表达式并不包括在变量对象中。

3. 不同执行上下文中的变量对象

执行上下文包括:全局上下文、函数上下文和 eval() 上下文。

全局上下文中的变量对象

这里我们先来了解一下什么是全局对象:

全局对象(global object)是指在进入任何执行上下文之前就已经创建了的对象。
这个对象只有一份,它的属性在程序中的任何地方都可以访问,全局对象的生命周期终止于程序退出的那一刻。

全局对象初始化时系统将创建并初始化一系列原始属性,例如:Math、String、Date、parseInt、window等等,之后是我们在全局上下文中自己定义的全局变量。在 DOM 中,全局对象的 window 属性可以引用全局对象自身,全局上下文环境的 this 属性也可以引用全局对象。

// 全局执行上下文环境
EC(globalContext) = {
   // 全局对象(全局上下文环境的变量对象) 
   global: {
      Math: <...>,
      String: <...>,
      ...
      ...
      window: global     // 引用全局对象自身
   },
   
   // this 属性
   this: global

   // 作用域链
   ...
}

举个例子:

var a = 10;

console.log(a);               // 10
console.log(window.a);        // 10
console.log(this.a);          // 10

因此,在全局上下文环境中,变量对象用全局对象来表示。

函数上下文中的变量对象

在函数上下文中,变量对象用活动对象 AO(Active Object)来表示。

VO(functionContext) = AO

活动对象是在进入函数上下文时刻被创建的,它是通过函数的 arguments 属性进行初始化。arguments 也是一个对象。

AO = {
   arguments: {
      ...
   }
}

arguments 是活动对象的一个属性,它也是一个对象,包括以下属性:

callee - 指向当前函数的引用

length - 真正传递的参数个数

properties-indexes - index 是字符串类型的整数,例如"1": "aa",类似于数组类型,也可以通过arguments[1]来访问,但是不能用数组的方法(push, pop等等)。另外,properties-indexes 的值和实际传递进来的参数之间是共享的,一个改变,另一个也随之改变。

举个例子:

function foo (x, y, z) {
  
   // 声明的函数参数数量
   console.log(foo.length);      // 3

   // 实际传递进来的参数数量
   console.log(arguments.length);      // 2

   // arguments 的 callee 属性指向当前函数
   console.log(arguments.callee === foo)   // true

   // 参数共享
   console.log(x === arguments[0]);      // true
   console.log(x);      // 10

   arguments[0] = 20;
   console.log(x);   // 20

   x = 30;
   console.log(arguments[0]);    // 30

   // 但是注意,没有传递进来的参数 z ,和第3个索引值是不共享的
   z = 40;
   console.log(arguments[2]);      // undefined

   arguments[2] = 50;
   console.log(z);      // 40
}

foo(10, 20);
4. 代码是如何被处理的

在第1节中我们讲过js 代码的编译过程,其中有一步叫作预编译,是说在代码执行前的几微秒会首先对代码进行编译,形成词法作用域,然后执行。

那么执行上下文的代码就就可以分成两个阶段来处理:

进入执行上下文(预编译)

执行代码

而变量对象的修改变化和这两个阶段是紧密相关的。
并且所有类型的执行上下文都会有这2个阶段。

进入执行上下文

当引擎进入执行上下文时(代码还未执行),VO 里已经包含了一些属性:

函数的所有形参(如果是函数执行上下文)

由名称和对应值组成的一个变量对象的属性被创建,如果没有传递对应的实参,那么由名称和 undefined 组成的一种变量对象的属性也会被创建。

所有的函数声明(Function Declaration - FD)

由名称和对应值(函数对象 function object)组成的一个变量对象的属性被创建,如果变量对象已经存在相同名称函数的属性,则完全替换这个属性。

所有的变量声明(Variable Declaration - var)

由名称和对应值(在预编译阶段所有变量值都是 undefined)组成的一个变量对象的属性被创建,如果变量名和已经声明的形参或者函数相同,则变量名不会干扰已经存在的这类属性,如果已经存在相同的变量名,则跳过当前声明的变量名。

注意:变量碰到相同名称的变量是忽略,函数碰到相同名称的函数是覆盖。

举个例子:

function foo (a, b) {
   var c = 5;

   function bar () {};

   var d = function _d () {};

   (function f () {});
}

foo(10);

当进入带有实参10的 foo 函数上下文时(预编译时,此时代码还没有执行),AO 结构如下:

AO(foo) = {
   a: 10,
   b: undefined,

   c: undefined,
   bar: ,
   d: undefined 
};

注意,函数表达式 f 并不包含在活动对象 AO 内。
也就是说,只有函数声明会被包含在变量对象 VO 里面,函数表达式并不会影响变量对象。

行内函数表达式 _d 则只能在该函数内部可以使用, 也不会包含在 VO 内。

这之后,就会进入第2个阶段,代码执行阶段。

代码执行

在这个阶段,AO/VO 已经有了属性(并不是所有的属性都有值,大部分属性的值还是系统默认的初始值 undefined)。

AO 在代码执行阶段被修改如下:

AO["c"] = 5;
AO["d"] = 

再次要提醒大家,因为函数表达式 _d 已经保存到了声明的变量 d 上面,所以变量 d 仍然存在于 VO/AO 中。我们可以通 d() 来执行函数。但是函数表达式 f 却不存在于 VO/AO 中,也就是说,如果我们想尝试调用 f 函数,不管在函数定义前还是定义后,都会出现一个错误"f is not defined",未保存的函数表达式只有在它自己的定义或递归中才能被调用。

再来一个经典例子:

console.log(x);      // function

var x = 10;
console.log(x);      // 10

x = 20;

function x () {};

console.log(x);      // 20

这里为什么是这样的结果呢?

上边我们说过,在代码执行之前的预编译,会为变量对象生成一些属性,先是形参,再是函数声明,最后是变量,并且变量并不会影响同名的函数声明。

所以,在进入执行上下文时,AO/VO 结构如下:

AO = {
   x: 

   // 在碰到变量声明 x 时,因为已经存在了函数声明 x ,所以会忽略
}

紧接着,在代码执行阶段,AO/VO 被修改如下:

AO["x"] = 10;
AO["x"] = 20;

希望大家可以好好理解变量对象,对于理解我们后边要讲的作用域链有很大的帮助。

5. 变量

有一些文章说过:

不管是使用 var 关键字(在全局上下文)还是不使用 var 关键字(在任何地方),都可以声明一个变量。

请记住,这是错误的观念。

任何时候,变量都只能通过使用 var 关键字来声明(ES6 之前)

a = 10;

上面的赋值语句,仅仅是给全局对象创建了一个新属性(在在非严格模式,严格模式下会报错),但注意,它不是变量。“不是变量”并不是说它不能被改变,而是指它不符合ECMAScript 规范中变量的概念。

让我们通过一个例子来看一下两者的区别:

console.log(a);        // undefined
console.log(b);        // 报错,b is not defined

b = 10;
var a = 20;

只要我们很好的理解了:变量对象、预编译阶段和执行代码阶段,就可以迅速的给出答案。

预编译(进入上下文)阶段:

VO = {
   a: undefined
}

我们可以看到,因为 b 不是通过 var 声明的,所以这个阶段根本就没有 b ,b 只有在代码执行阶段才会出现。但是在这个例子中,还没有执行到 b 那就已经报错了。

我们稍微更改一下示例代码:

console.log(a);      // undefined

b = 10;
console.log(b);             // 10 代码执行阶段被创建
console.log(window.b);      // 10
console.log(this.b);        // 10

var a = 20;
console.log(a);      // 20 代码执行阶段被修改

关于变量,还有一个很重要的知识点。

变量不能用 delete 操作符来删除。

a = 10;

console.log(window.a);    // 10

console.log(delete a);    // true

console.log(window.a);    // undefined

var b = 20;
console.log(window.b);    // 20

console.log(delete b);    // false

console.log(window.b);    // 20

注意:这个规则在 eval() 上下文中不起作用。

eval("var a = 10;");
console.log(window.a);    // 10

console.log(delete a);    // true

console.log(window.a);    // undefined
6. 总结

这一节中我们讲了变量对象,下一节就是我们的重头戏 - 作用域链。希望大家可以持续关注我,我们一起进步。

欢迎关注我的公众号

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81953.html

相关文章

  • Spring IOC 容器源码分析系列文章导读

    摘要:本文是容器源码分析系列文章的第一篇文章,将会着重介绍的一些使用方法和特性,为后续的源码分析文章做铺垫。我们可以通过这两个别名获取到这个实例,比如下面的测试代码测试结果如下本小节,我们来了解一下这个特性。 1. 简介 Spring 是一个轻量级的企业级应用开发框架,于 2004 年由 Rod Johnson 发布了 1.0 版本。经过十几年的迭代,现在的 Spring 框架已经非常成熟了...

    NSFish 评论0 收藏0
  • 听飞狐聊JavaScript设计模式系列06

    本回内容介绍 上一回聊到JS中模拟接口,装饰者模式,掺元类,分析了backbone的继承源码,感觉还好吧! 介一回,偶们来聊一下在JS单例模式(singleton),单例模式其实运用很广泛,比如:jquery,AngularJS,underscore吖虾米的都是单例模式,来吧,直接开始咯: 1. 单例模式 保证一个类只有一个实例,从全局命名空间里提供一个唯一的访问点来访问该对象。其实之前写过的对象...

    hiYoHoo 评论0 收藏0
  • 【ES6脚丫系列】模块Module

    摘要:命令用于规定本模块的对外接口。空格模块名字符串。其他模块加载该模块时,命令可以为该匿名函数指定任意名字。写法函数声明命令用在非匿名函数前,也是可以的。加载的时候,视同匿名函数加载。 本文字符数8200+,阅读时间约16分钟。 『ES6知识点总结』模块Module 第一节:Module基本概念 【01】过去使用CommonJS和AMD,前者用于服务器,后者用于浏览器。 Module可以取...

    gotham 评论0 收藏0
  • WebAssembly 系列(四)WebAssembly 工作原理

    摘要:但是它们其实并不是二选一的关系并不是只能用或者。正因为如此,指令有时也被称为虚拟指令。这是因为是采用基于栈的虚拟机的机制。声明模块的全局变量。。下文预告现在你已经了解了模块的工作原理,下面将会介绍为什么运行的更快。 作者:Lin Clark 编译:胡子大哈 翻译原文:http://huziketang.com/blog/posts/detail?postId=58c77641a6d8...

    stormzhang 评论0 收藏0
  • 前端培训-中级阶段(8)- jQuery元素属性样式操作(2019-08-01期)

    摘要:前端最基础的就是。对应,是标签的属性。获取匹配元素相对父元素的偏移。返回的对象包含两个整型属性和。一组包含作为动画属性和终值的样式属性和及其值的集合动画的额外选项。指示是否在效果队列中放置动画。 前端最基础的就是 HTML+CSS+Javascript。掌握了这三门技术就算入门,但也仅仅是入门,现在前端开发的定义已经远远不止这些。前端小课堂(HTML/CSS/JS),本着提升技术水平,...

    everfly 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<