资讯专栏INFORMATION COLUMN

FE.ES-理解ECMA Javascript作用域

shenhualong / 1670人阅读

摘要:赋值操作符会导致查询。中有两个机制可以欺骗词法作用域和。如果是作为赋值表达式的一部分的话,那它就是一个函数表达式,如果被包含在一个函数体内,或者位于程序的最顶部的话,那它就是一个函数声明。但函数不是唯一的作用域单元。

本文仅整理自己所学做为笔记,如有错误请指正。

作用域

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。赋值操作符会导致 LHS 查询。 = 操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。

JavaScript 引擎首先会在代码执行前对其进行编译,在这个过程中,像 var a = 2 这样的声明会被分解成两个独立的步骤:

首先, var a 在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。

接下来, a = 2 会查询(LHS 查询)变量 a 并对其进行赋值。

LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。

不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛出 ReferenceError 异常(严格模式下)。

词法作用域

词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。

JavaScript 中有两个机制可以“欺骗”词法作用域: eval(..) 和 with 。前者可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。

这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。

因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。

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

var foo = checkscope();
foo();//local scope
函数表达式和函数声明

函数声明:function 函数名称 (参数:可选){ 函数体 }
函数表达式:function 函数名称(可选)(参数:可选){ 函数体 }

辨别:

如果不声明函数名称,它肯定是表达式。

如果function foo(){}是作为赋值表达式的一部分的话,那它就是一个函数表达式,如果function foo(){}被包含在一个函数体内,或者位于程序的最顶部的话,那它就是一个函数声明。

被括号括住的(function foo(){}),他是表达式的原因是因为括号 ()是一个分组操作符,它的内部只能包含表达式

  function foo(){} // 声明,因为它是程序的一部分

  (function(){
    function bar(){} // 声明,因为它是函数体的一部分
  })();

  var bar = function foo(){}; // 表达式,因为它是赋值表达式的一部分
  new function bar(){}; // 表达式,因为它是new表达式
  (function foo(){}); // 表达式:包含在分组操作符内
  
  try {
    (var x = 5); // 分组操作符,只能包含表达式而不能包含语句:这里的var就是语句
  } catch(err) {
    // SyntaxError
  }

函数声明在条件语句内虽然可以用,但是没有被标准化,最好使用函数表达式

函数声明会覆盖变量声明,但不会覆盖变量赋值

function value(){
    return 1;
}
var value;
alert(typeof value);    //"function"
函数作用域和块作用域

函数是 JavaScript 中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。但函数不是唯一的作用域单元。

块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指 { .. } 内部)。

从 ES3 开始, try/catch 结构在 catch 分句中具有块作用域。

在 ES6 中引入了 let 关键字( var 关键字的表亲),用来在任意代码块中声明变量。
if(..) { let a = 2; } 会声明一个劫持了 if 的 { .. } 块的变量,并且将变量添加到这个块中。

有些人认为块作用域不应该完全作为函数作用域的替代方案。两种功能应该同时存在,开发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。

提升

我们习惯将 var a = 2; 看作一个声明,而实际上 JavaScript 引擎并不这么认为。它将 var a和 a = 2 当作两个多带带的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。

这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。

声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。
要注意避免重复声明,特别是当普通的 var 声明和函数声明混合在一起的时候,否则会引
起很多危险的问题!

var a;
if (!("a" in window)) {
    a = 1;
}
alert(a);
作用域闭包

通常,程序员会错误的认为,只有匿名函数才是闭包。其实并非如此,正如我们所看到的 —— 正是因为作用域链,使得所有的函数都是闭包(与函数类型无关: 匿名函数,FE,NFE,FD都是闭包), 这里只有一类函数除外,那就是通过Function构造器创建的函数,因为其[[Scope]]只包含全局对象。 为了更好的澄清该问题,我们对ECMAScript中的闭包作两个定义(即两种闭包):

ECMAScript中,闭包指的是:

从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
从实践角度:以下函数才算是闭包:
即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
在代码中引用了自由变量

循环闭包
for (var i=1; i<=5; i++) {
    (function(j) {
        setTimeout( function timer() {
        console.log( j );
        }, j*1000 );
    })( i );
}

for (var i=1; i<=5; i++) {
    let j = i; // 是的,闭包的块作用域!
    setTimeout( function timer() {
    console.log( j );
    }, j*1000 );
}

for (let i=1; i<=5; i++) {
    setTimeout( function timer() {
    console.log( i );
    }, i*1000 );
}
var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();//3
data[1]();//3
data[2]();//3
模块

模块有两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回
值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭
包。

现代模块机制
var MyModules = (function Manager() {
    var modules = {};
    function define(name, deps, impl) {
        for (var i=0; i
未来模块机制
//bar.js
function hello(who) {
    return "Let me introduce: " + who;
}
export hello;
//foo.js
// 仅从 "bar" 模块导入 hello()
import hello from "bar";
var hungry = "hippo";
function awesome() {
    console.log(
        hello( hungry ).toUpperCase()
    );
}
export awesome;
baz.js
// 导入完整的 "foo" 和 "bar" 模块
module foo from "foo";
module bar from "bar";
console.log(
    bar.hello( "rhino" )
); // Let me introduce: rhino
foo.awesome(); // LET ME INTRODUCE: HIPPO
块作用域替代方案

Google 维护着一个名为 Traceur 的项目,该项目正是用来将 ES6 代码转换成兼容 ES6 之前的环境(大部分是 ES5,但不是全部)。TC39 委员会依赖这个工具(也有其他工具)来测试他们指定的语义化相关的功能。

{
    try {
        throw undefined;
    } catch (a) {
        a = 2;
        console.log( a );
    }
}
console.log( a )
上下文 EC(执行环境或者执行上下文,Execution Context)
EC={
    VO:{/* 函数中的arguments对象, 参数, 内部的变量以及函数声明 */},
    this:{},
    Scope:{ /* VO以及所有父执行上下文中的VO */}
}
ECS(执行环境栈Execution Context Stack)
//ECS=[Window]
A(//ECS=[Window,A]
    B(//ECS=[Window,A,B]
        //run B 
    )
    //ECS=[Window,A]
)
//ECS=[Window]
VO(变量对象,Variable Object)
var a = 10;
function test(x) {
  var b = 20;
};
test(30);
/*
VO(globalContext)
  a: 10,
  test: 
VO(test functionContext)
  x: 30
  b: 20
*/
AO(活动对象,Active Object)
function test(a, b) {
  var c = 10;
  function d() {}
  var e = function _e() {};
  (function x() {});
} 
test(10);
/*
AO(test) = {
  a: 10,
  b: undefined,
  c: undefined,
  d: 
  e: undefined
};
*/
scope chain(作用域链)和[[scope]]属性
Scope = AO|VO + [[Scope]]
例子
var x = 10;
 
function foo() {
  var y = 20;
 
  function bar() {
    var z = 30;
    alert(x +  y + z);
  }
 
  bar();
}
 
foo(); // 60

全局上下文的变量对象是:

globalContext.VO === Global = {
  x: 10
  foo: 
};

在“foo”创建时,“foo”的[[scope]]属性是:

foo.[[Scope]] = [
  globalContext.VO
];

在“foo”激活时(进入上下文),“foo”上下文的活动对象是:

fooContext.AO = {
  y: 20,
  bar: 
};

“foo”上下文的作用域链为:

fooContext.Scope = fooContext.AO + foo.[[Scope]] // i.e.:
 
fooContext.Scope = [
  fooContext.AO,
  globalContext.VO
];

内部函数“bar”创建时,其[[scope]]为:

bar.[[Scope]] = [
  fooContext.AO,
  globalContext.VO
];

在“bar”激活时,“bar”上下文的活动对象为:

barContext.AO = {
  z: 30
};

“bar”上下文的作用域链为:

barContext.Scope = barContext.AO + bar.[[Scope]] // i.e.:
 
barContext.Scope = [
  barContext.AO,
  fooContext.AO,
  globalContext.VO
];

对“x”、“y”、“z”的标识符解析如下:

- "x"
-- barContext.AO // not found
-- fooContext.AO // not found
   globalContext.VO // found - 10

- "y"
-- barContext.AO // not found
   fooContext.AO // found - 20

- "z"
   barContext.AO // found - 30

参考资料:
深入理解JavaScript系列(16):闭包(Closures)
JavaScript深入之执行上下文栈
JavaScript深入之词法作用域和动态作用域
你不懂JS:作用域与闭包
变量对象(Variable object)
深入理解JavaScript系列(14):作用域链(Scope Chain)
let 是否会声明提升?

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

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

相关文章

  • FE.ES-理解ECMA Javascript作用

    摘要:赋值操作符会导致查询。中有两个机制可以欺骗词法作用域和。如果是作为赋值表达式的一部分的话,那它就是一个函数表达式,如果被包含在一个函数体内,或者位于程序的最顶部的话,那它就是一个函数声明。但函数不是唯一的作用域单元。 本文仅整理自己所学做为笔记,如有错误请指正。 作用域 作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用 LHS 查...

    newsning 评论0 收藏0
  • FE.ES-理解ECMA Javascript的this

    摘要:捕获所有参数绑定当一个函数用作构造函数时使用关键字,它的被绑定到正在构造的新对象。使用来调用函数,或者说发生构造函数调用时,会自动执行下面的操作你不知道的创建或者说构造一个全新的对象。在箭头函数中,与封闭词法上下文的保持一致。 this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数的调用位置(也就是函数的调用方法)。 四条规则:(你不知道的JS) 1. 默认绑定 func...

    Meils 评论0 收藏0
  • FE.ES-理解ECMA Javascript的this

    摘要:捕获所有参数绑定当一个函数用作构造函数时使用关键字,它的被绑定到正在构造的新对象。使用来调用函数,或者说发生构造函数调用时,会自动执行下面的操作你不知道的创建或者说构造一个全新的对象。在箭头函数中,与封闭词法上下文的保持一致。 this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数的调用位置(也就是函数的调用方法)。 四条规则:(你不知道的JS) 1. 默认绑定 func...

    elliott_hu 评论0 收藏0

发表评论

0条评论

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