JavaScript词法做用域—你不知道的JavaScript上卷读书笔记(一)

前段时间在天天往返的地铁上抽空将 《你不知道的JavaScript(上卷)》读了一遍,这本书不少部分写的非常精妙,对于接触前端时间不过久的人来讲,就好像是叩开了JavaScript的另外一扇门,不少内容醍醐灌顶!因此决定将这本书分四个部分整理出来,同时也这本书强烈推荐给正在进阶的小伙伴们。这篇博文主要整理第一部分 做用域前端

词法做用域

理解做用域

首先要介绍下JS参与程序 var a = 2的处理过程的演员表:安全

  • 引擎函数

    从头至尾负责整个JavaScript 程序的编译及执行过程。性能

  • 编译器this

    引擎的好朋友之一,负责语法分析及代码生成等脏活累活调试

  • 做用域code

    引擎的另外一位好朋友,负责收集并维护由全部声明的标识符(变量)组成的一系列查询,并实施一套很是严格的规则,肯定当前执行的代码对这些标识符的访问权限。对象

在处理过程当中,引擎会为变量a 进行LHS 查询。另一个查找的类型叫做RHS当变量出如今赋值操做的左侧时进行LHS 查询,出如今右侧时进行RHS 查询。递归

console.log(a)  // RHS查询
引擎与做用域的对话
function foo(a) {
    console.log( a ); // 2
}
foo( 2 );

引擎:我说做用域,我须要为foo 进行RHS 引用。你见过它吗?
做用域:别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。
引擎:哥们太够意思了!好吧,我来执行一下foo。
引擎:做用域,还有个事儿。我须要为a 进行LHS 引用,这个你见过吗?
做用域:这个也见过,编译器最近把它声名为foo 的一个形式参数了,拿去吧。
引擎:大恩不言谢,你老是这么棒。如今我要把2 赋值给a。
引擎:哥们,很差意思又来打扰你。我要为console 进行RHS 引用,你见过它吗?
做用域:咱俩谁跟谁啊,再说我就是干这个。这个我也有,console 是个内置对象。
给你。
引擎:么么哒。我得看看这里面是否是有log(..)。太好了,找到了,是一个函数。
引擎:哥们,能帮我再找一下对a 的RHS 引用吗?虽然我记得它,但想再确认一次。
做用域:放心吧,这个变量没有变更过,拿走,不谢。
引擎:真棒。我来把a 的值,也就是2,传递进log(..)。
做用域嵌套

当一个块或函数嵌套在另外一个块或函数中时,就发生了做用域的嵌套。所以,在当前做用域中没法找到某个变量时,引擎就会在外层嵌套的做用域中继续查找,直到找到该变量,或抵达最外层的做用域(也就是全局做用域)为止。事件

两个常见异常
  1. 若是RHS 查询在全部嵌套的做用域中遍寻不到所需的变量,引擎就会抛出ReferenceError异常。值得注意的是,ReferenceError 是很是重要的异常类型。

  2. 若是RHS 查询找到了一个变量,可是你尝试对这个变量的值进行不合理的操做,好比试图对一个非函数类型的值进行函数调用,或着引用null 或undefined 类型的值中的属性,那么引擎会抛出另一种类型的异常,叫做TypeError。

ReferenceError 同做用域判别失败相关,而TypeError 则表明做用域判别成功了,可是对结果的操做是非法或不合理的。



大部分标准语言编译器的第一个工做阶段叫做词法化,词法做用域就是定义在词法阶段的做用域.

function foo(a) {
    var b = a * 2;
    function bar(c) {
        console.log( a, b, c );
    }
    bar( b * 3 );
}
foo( 2 ); // 2, 4, 12

上面例子中,有三个嵌套的做用域:

  1. 整个全局做用域,其中只有一个标识符:foo。
  2. 包含着foo 所建立的做用域,其中有三个标识符:a、bar 和b。
  3. 包含着bar 所建立的做用域,其中只有一个标识符:c。

做用域查找会在找到第一个匹配的标识符时中止


欺骗词法

有些函数会在运行时修改词法做用域,可是欺骗词法做用域会致使性能降低。

  • eval

在执行eval(..) 以后的代码时,引擎并不“知道”或“在乎”前面的代码是以动态形式插入进来,并对词法做用域的环境进行修改的。引擎只会如往常地进行词法做用域查找。

function foo(str, a) {
    eval( str ); // 欺骗!
    console.log( a, b );
}   
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3

eval(..) 调用中的"var b = 3;" 这段代码会被看成原本就在那里同样来处理。因为那段代码声明了一个新的变量b,所以它对已经存在的foo(..) 的词法做用域进行了修改。事实上,和前面提到的原理同样,这段代码实际上在foo(..) 内部建立了一个变量b,并遮蔽了外部(全局)做用域中的同名变量。可是在严格模式下,eval(..) 在运行时有其本身的词法做用域,意味着其中的声明没法修改所在的做用域。

  • with

    function foo(obj) {
         with (obj) {
             a = 2;
         }
     }
    
     var o2 = {
         b: 3
     };
    
     foo( o2 );
     console.log( o2.a ); // undefined
     console.log( a ); // 2——很差,a 被泄漏到全局做用域上了!

当咱们将o2 做为做用域时,其中并无a 标识符,所以进行了正常的LHS 标识符查找。o2 的做用域、foo(..) 的做用域和全局做用域中都没有找到标识符a,所以当a=2 执行时,自动建立了一个全局变量(由于是非严格模式)。

eval(..) 函数若是接受了含有一个或多个声明的代码,就会修改其所处的词法做用域,而with 声明其实是根据你传递给它的对象凭空建立了一个全新的词法做用域。不推荐使用eval(..) 和with 的缘由是会被严格模式所影响(限制)。with 被彻底禁止,而在保留核心功能的前提下,间接或非安全地使用eval(..) 也被禁止了。

函数做用域与块做用域

函数做用域

在任意代码片断外部添加包装函数,能够将内部的变量和函数定义“隐藏”起来,外部做用域没法访问包装函数内部的任何内容。

  1. 函数声明与函数表达式

    区分函数声明和表达式最简单的方法是看function 关键字出如今声明中的位置(不只仅是一行代码,而是整个声明中的位置)。若是function 是声明中的第一个词,那么就是一个函数声明,不然就是一个函数表达式。

    函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。

匿名与具名

setTimeout( function() {
        console.log("I waited 1 second!");
    }, 1000 );

这叫做匿名函数表达式,由于function().. 没有名称标识符。函数表达式能够是匿名的,而函数声明则不能够省略函数名——在JavaScript 的语法中这是非法的。

匿名函数的弊端:

  1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
  2. 若是没有函数名,当函数须要引用自身时只能使用已通过期的arguments.callee 引用,好比在递归中。另外一个函数须要引用自身的例子,是在事件触发后事件监听器须要解绑自身
  3. 匿名函数省略了对于代码可读性/ 可理解性很重要的函数名。一个描述性的名称可让代码不言自明。

    setTimeout( function timeoutHandler() { // <-- 快看,我有名字了!
         console.log( "I waited 1 second!" );
     }, 1000 );

当即执行函数表达式(IIFE)

var a = 2;
(function IIFE( global ) {
    var a = 3;
    console.log( a ); // 3
    console.log( global.a ); // 2
})( window );
console.log( a ); // 2

块级做用域

for (var i=0; i<10; i++) {
    console.log( i );
}   
//  为何要把一个只在for 循环内部使用(至少是应该只在内部使用)的变量i 污染到整个函数做用域中呢?

经常使用的块级做用域:

  1. with
  2. try/catch

    try {
         undefined(); // 执行一个非法操做来强制制造一个异常
     }
     catch (err) {
         console.log( err ); // 可以正常执行!
     }
     console.log( err ); // ReferenceError: err not found
  3. let/const (这两为ES6最基本的关键字,就很少介绍了,可是很重要!)

提高

先有鸡仍是先有蛋

a = 2;
var a;
console.log( a );

上面代码你认为会输出什么?不少开发者会认为是undefined,由于var a 声明在a = 2 以后,他们天然而然地认为变量被从新赋值了,所以会被赋予默认值undefined。可是,真正的输出结果是2。包括变量和函数在内的全部声明都会在任何代码被执行前首先被处理。

当你看到var a = 2; 时,可能会认为这是一个声明。但JavaScript 实际上会将其当作两个声明:var a; 和a = 2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。

刚才的代码会被处理为:

var a;        //提高
a = 2;
console.log( a );
先有蛋(声明)后有鸡(赋值)

函数优先

函数声明和变量声明都会被提高。可是一个值得注意的细节(这个细节能够出如今有多个“重复”声明的代码中)是函数会首先被提高,而后才是变量。考虑如下代码:

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

会输出1 而不是2 !这个代码片断会被引擎理解为以下形式:

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

最后提一点:js只有词法做用域,无动态做用域。可是this 机制某种程度上很像动态做用域。他们主要区别:词法做用域是在写代码或者说定义时肯定的,而动态做用域是在运行时肯定的。(this 也是!)词法做用域关注函数在何处声明,而动态做用域关注函数从何处调用。

相关文章
相关标签/搜索