2

本文为《你不知道的JavaScript(上卷)》中关于作用域相关的知识点的总结。

作用域

赋值操作

变量的赋值操作实际上有两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就对它进行赋值。

LHS以及RHS

在运行时引擎会在作用域中查找该变量

引擎对变量所做的查找分为LHS查询以及RHS查询LR分别代表一个赋值操作的左侧以及右侧。

讲的稍微精确一点:RHS查询与简单地查找某个变量的值别无二致,而LHS则是试图查找到变量的容器本身,从而对其进行赋值。

RHS可以理解成retrieve his source value(取到其源值),这意味着“得到某某的源值”。

深入一点:

console.log(a);

上诉代码对于a的引用就是一个RHS引用,即查找然后取到a的值。

相比之下,

a = 2;

这里的a就是一个RHS引用。

我们可以简单的记忆:

当变量出现在赋值操作的左侧时进行LHS查询,出现在赋值操作的右侧时进行RHS查询.

注意:作用域查找会在找到第一个匹配的标识符时停止

作用域嵌套

作用域是根据名称查找变量的一套规则

作用域嵌套的定义如下:

当一个块或者函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。

理解作用域嵌套这一机制,我们就可以理解变量查找的顺序:

  1. 在当前作用域查找变量。如果没有,则进行下一步

  2. 判断是否是全局作用域。如果是,则停止查找过程;如果不是,则进行下一步

  3. 进入当前作用域的外层作用域,并进行第一步

形象一点,我们可以把作用域查找想象成在大楼中找人。

第一层代表当前作用域,大楼的顶层代表全局作用域。

首先在当前楼层查找,如果没有找到,则上一楼进行查找,一直到找到这个人或者找完整个大楼依然没有找到为止。

异常报错的种类

如果能将LHS以及RHS进行很好的区分,那我们就能够很好的理解浏览器所抛出的各种异常。

下举几种特别常见的报错:

  • ReferenceError:

    1. RHS查询变量未找到值

    2. 严格模式LHS查询失败

  • TypeError:

    1. RHS找到该变量值,但尝试对这个变量的值进行不合理的操作(例如,引用null或者undefined类型的值中的属性)

词法作用域

词法作用域完全由写代码期间函数所声明的位置来定义

欺骗词法作用域

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

eval

eval() 是一个危险的函数, 他执行的代码拥有着执行者的权利。如果你运行eval()伴随着字符串,那么你的代码可能被恶意方(不怀好意的人)影响, 通过在使用方的机器上使用恶意代码,可能让你失去在网页或者扩展程序上的权限。更重要的是,第三方代码可以看到作用域在某一个eval()被调用的时候,这有可能导致一些不同方式的攻击。相似的Function就是不容易被攻击的。

with

根据你所传递给它的对象凭空创建了一个全新的词法作用域

性能问题

欺骗词法作用域会导致性能下降,其原因在于编译阶段的性能优化不起作用

JavaScript引擎会在即时编译阶段(during the compilation phase)进行数项的性能优化。其中的某些优化依赖于能够根据代码的词法进行静态分析,并预先确定所有变量和函数的定义位置,才能在执行的过程中快速找到标识符。

但是,编译到含有evalwith的代码时,编译器无法知道eval或者with会接受什么代码,自然无法做代码优化。

函数作用域以及块作用域

函数作用域:属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。

隐藏组件内部实现

开发者最主要是利用函数作用域实现隐藏组件或者API的内部实现,最小限度的暴露必要内容。

比如对于一些组件的开发,大家习惯于利用立即执行函数(function() {})()进行内部实现的封装。

规避冲突

利用函数作用域将变量保持在私有、无冲突的作用域中,这样可以有效规避掉所有的冲突。

举个例子,underscore这个库里面有跟原生js一样的方法map,那怎么区分这两个方法呢?通过将map当做一个属性挂载在underscore上面,这样可以避免两者的冲突。

立即执行函数表达式

形式如下:

  1. (function() {...})()

  2. (function() {...})()

上面两种形式没有区别,可依个人兴趣随意使用。

立即执行函数表达式的一种进阶用法就是把它们当做函数调用并传递参数进去。

各种类库常见的用法是:

(function(global) {
    ...
})(window)

块作用域

块作用域目前在ES6中有如下体现:

  1. let

  2. const

  3. with:用with从对象创建出的作用域仅在with声明而非外部作用域中有效。

  4. try/catchcatch分句会创建一个块作用域,其中声明的变量仅在catch内部有效。

例如:

for (let i; i < 4; i ++) {
    ...
}

console.log(i) // Uncaught ReferenceError: i is not defined
try {
    undefined();
} catch (err) {
    console.log(err);
}

console.log(err); // Uncaught ReferenceError: err is not defined

作用域闭包

知乎上面有关于闭包的问题:什么是闭包?

其中寸志老师的解释我认为是比较好的。

对于闭包,《你不知道的JavaScript(上卷)》这本书的解释是:

当函数可以记住并访问所在的词法作用域时,就产生了闭包。

我们实际上来理解闭包时,需要特别注意是两个点:函数作用域

简单的来说,就是函数以及作用域的结合,注意,作用域必须是封闭的,其主要的表现形式就是函数中返回一个函数。

闭包在类库、组件封装中有太多的示例了,本文就不拓展了。

块作用域与闭包的结合

首先看一个单纯的闭包的代码:

for (var i = 0; i <= 5; i++) {
    (function() {
       var j = i;
       setTimeout(function timer(){
           console.log(j);
       }, j * 1000) 
    })()
}

这段代码就是在每次循环的时候创建一个新的封闭作用域,保存当次循环的i值。

再看一下下面的代码:

for (let i = 0; i <= 5; i++) {
    setTimeout(function timer(){
        console.log(i);
    }, i*1000)
}

利用let创建块作用域,当块作用域与闭包结合之后,我们可以减少创建新的封闭作用域这一操作(var j = i);

that's cool!

动态词法作用域

动态作用域链是基于调用栈的,而不是代码中的作用域嵌套。

对于JavaScript,不存在动态作用域。如果一定要找一个点与动态词法作用域扯上关系的话,那就是this值了。this值打算在下一篇文章中详解。

变量提升

举个最简单的例子:

alert(a); // undefined
var a = 12;

有同样作用的是函数声明function,例如:

alert(func); // function func(){}
function func() {};

但是函数表达式不会提升:

foo(); // TypeError

var foo = function bar() {
    ...
}

注意:仅有var和函数声明function才可以变量提升。

函数声明与函数表达式的区别:

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

ES6中新增的let以及const关键字不可以进行变量提升,我们可以尝试一下:

// 1. let
alert(a); // Uncaught ReferenceError: a is not defined
let a = 'abc';

// 2. const
alert(b); // Uncaught ReferenceError: b is not defined
const b = 123;

函数优先

先来看下面的代码:

foo(); // 1
var foo;

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

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

上面的例子说明:
函数会被首先提升,然后才是变量

上面的代码实际等于:

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

foo(); // 1

var foo;

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

模块

模块这一利器,在以前封装插件用的非常多,示例如下:

var foo = (function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    
    function doSomething() {
        console.log(something)
    }
    
    function doAnother() {
        console.log(another.join("!"));
    }
    
    return  {
        doSomething: doSomething,
        doAnother: doAnother
    }
})()

模块模式必备条件如下:

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的莫模块实例)。

  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。

当然,说到模块,我们不得不提到CMDAMDES6 module等模块机制了。

知乎上有提到AMD 和 CMD 的区别有哪些?

我这里简单提一下两者的区别:

  • AMD:

    • early executing(提前执行)

    • 推荐依赖前置

    • 示例:requireJs

  • CMD:

    • as lazy as possible(延迟执行)

    • 推荐依赖就近

    • 示例:seaJs

继续聊一下ES6的模块机制(importexport)。

import可以将一个模块中的一个或多个API导入到当前的作用域中,并分别绑定在一个变量上。

export会将当前模块的一个标识符(变量、函数)导出为公共API。

Github有很多基于es6实现的代码功能,请自行查阅。

结语

好了,作用域相关的点整理完了,我将其中主要分成三部分:

  1. 作用域

  2. 提升

  3. 模块

如果有遗漏,欢迎指正~


望舒
2.3k 声望133 粉丝

an unexamined life is a life not worth living