【JavaScript】做用域 - 笔记

什么是做用域

做用域是为了在咱们使用变量引用以后,更方便的寻找到这些变量而制定的一套规则。程序员

简单来讲,做用域就是变量的使用范围,且同一个做用域内的变量是惟一的。bash

做用域嵌套

做用域在实际的使用中,会互相的嵌套,因此咱们一般须要顾及多个做用域。闭包

当一个块或者函数嵌套在另外一个块或者函数中,就发生了做用域的嵌套。在当前做用域没法找到某个变量时,引擎会在外层的做用域中寻找,逐级递增出去,直到找到该变量或者已经抵达全局做用域。函数

最外层的做用域是全局做用域。工具

LHS和RHS

在理解做用域的时候,咱们还须要对LHSRHS有所了解。由于在变量还未声明的状况下,LHSRHS的查询方式是不同的。性能

在未使用**“严格模式”**的状况下,LHS在未找到目标变量时,会建立一个对应名称的变量而后使用。而RHS只要未查询到目标变量,就会直接报错。学习

当变量出如今赋值操做的左侧时进行 LHS 查询, 出如今右侧时进行 RHS 查询。讲得更准确一点, RHS 查询与简单地查找某个变量的值别无二致, 而 LHS 查询则是试图找到变量的容器自己, 从而能够对其赋值。 从这个角度说, RHS 并非真正意义上的“赋值操做的右侧”, 更准确地说是“非左侧”。优化

简单理解。LHS查找的是容器,RHS查找的是容器里面的内容。ui

例如: var a = 1spa

a是容器,咱们要将=1这个赋值,赋值到容器a上面,这个操做并不须要a本来容器里面是什么,不管是什么都覆盖掉便可。

console.log(a) 这里的操做,须要将a容器里面的值取出来而后打印出来。

词法做用域

做用域共有两种主要的工做模型。分别是最广泛的词法做用域和比较少见的动态做用域。

这里咱们只讨论词法做用域。

JavaScript的做用域,就是词法做用域。

大部分标准语言编译器的第一个工做阶段叫词法化。词法做用域就是定义在词法阶段的做用域。 因此词法做用域就是由你写代码时的变量和做用域决定的。

随便举个例子:

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

}
foo(2); //2,4,12
复制代码

上述例子中,出现了三个做用域,分别是:

  1. 全局做用域
  2. foo()方法内部的做用域
  3. bar()方法内部的做用域

JavaScript的做用域是严格包含的,没有任何函数能够部分地同时出如今两个父级函数中。

欺骗词法

JavaScript中,有两种机制可让代码在运行的时候来“修改”(或者说欺骗)词法做用域。

须要注意的是,欺骗词法做用域会致使性能降低。

eval

JavaScript中的eval()函数能够接受一个字符串为参数,并将其中的内容视为书写时就存在于某个位置中的代码。

举个例子:

function foo(str,a){
    eval(str);
    console.log(a,b);
}
var b = 2;
foo( 'var b = 3;', 1 ); // 1,3
console.log(b); //2
复制代码

能够看到,这个例子中,eval()的参数为var b = 3。在全局做用域中,自己已经将b变量的值声明为2。可是经过eval()方法,将foo()方法中所调用到的b参数的值,改成了3

在调用完eval()以后,咱们在全局做用域中,再次打印b参数的值,发现依旧是2

那么咱们再看一个例子:

function foo(str){
    eval(str);
    console.log(a);
}

foo('var a = 2'); // 2

console.log(a); // ReferenceError: a is not defined
复制代码

结合两个例子咱们能够看到,eval()方法的参数传入的声明,只会在调用对应方法的时候有效。实际做用域中并不会永久性的生成或者改变对应的声明。 能够理解为临时声明。

with

JavaScript中,还可使用with关键字来欺骗词法做用域。

with一般被当作重复引用同一个对象中的多个属性的快捷方式,能够不须要重复引用对象自己。

例子:

function foo(obj) {
    with (obj) {
        a = 2;
    }
}

var obj = {
	a:1
}

foo(obj);

console.log(obj) // {a:2}
复制代码

在上述例子中,咱们能够看到,调用with声明以后,修改的内容会泄露到全局做用域上。、

with声明其实是根据你传递的对象,凭空建立了一个全新的词法做用域。

小结

词法做用域意味着做用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段 基本可以知道所有标识符在哪里以及是如何声明的,从而可以预测在执行过程当中如何对它 们进行查找。 JavaScript 中有两个机制能够“欺骗”词法做用域: eval(..) 和 with 。前者能够对一段包 含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法做用域(在 运行时)。后者本质上是经过将一个对象的引用看成做用域来处理,将对象的属性看成做 用域中的标识符来处理,从而建立了一个新的词法做用域(一样是在运行时)。 这两个机制的反作用是引擎没法在编译时对做用域查找进行优化,由于引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都将致使代码运行变慢。不要使用它们。

函数做用域和块做用域

JavaScript的做用域,主要由函数做用域块做用域组成。

函数中的做用域

JavaScript 具备基于函数的做用域,每建立一个函数,就会建立一个对应的做用域。

函数做用域的含义是指,属于这个函数的所有变量均可以在整个函数的范围内使用以及复用。

隐藏内部实现

对函数的传统认知就是先声明一个函数,而后再向里面添加代码。但反过来想也能够带来 一些启示:从所写的代码中挑选出一个任意的片断,而后用函数声明对它进行包装,实际 上就是把这些代码“隐藏”起来了。

实际上,“隐藏”这个操做,远比咱们想象的做用更大。

隐藏部分变量或者函数,符合最小受权或暴露原则。避免过多的变量向外泄露。

咱们须要遵照的一个原则是,尽可能让变量或者函数,只让其在须要使用的范围内出现。

规避冲突

“隐藏”所带来的另外一个好处,是能够避免同名标识符之间的冲突,避免变量的值被意外覆盖。

毕竟程序员烦恼的事情之一,是如何给众多类似且重复的变量命名。

  1. 全局命名空间 变量冲突的一个典型例子存在于全局做用域中。当程序中加载了多个第三方库时,若是它们没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引起冲突。 这些库一般会在全局做用域中声明一个名字足够独特的变量,一般是一个对象。这个对象被用做库的命名空间,全部须要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将本身的标识符暴漏在顶级的词法做用域中。 例如:
var MyReallyCoolLibrary = {
    awesome: "stuff",
    doSomething: function() {
        // ...
    },
    doAnotherThing: function() {
        // ...
    }
}
复制代码
  1. 模块管理 另一种避免冲突的办法和现代的模块机制很接近,就是从众多模块管理器中挑选一个来使用。使用这些工具,任何库都无需将标识符加入到全局做用域中,而是经过依赖管理器的机制将库的标识符显式地导入到另一个特定的做用域中。 显而易见,这些工具并无可以违反词法做用域规则的“神奇”功能。它们只是利用做用域的规则强制全部标识符都不能注入到共享做用域中,而是保持在私有、无冲突的做用域中,这样能够有效规避掉全部的意外冲突。 所以,只要你愿意,即便不使用任何依赖管理工具也能够实现相同的功效。

函数做用域

“隐藏”变量或函数,这个技术虽然能够解决一些问题,可是并不理想。

首先必须声明一个具名函数,其次咱们必须显式的经过函数名去调用这个函数,才能够运行其中的代码。

为此,JavaScript提供了能够同时解决这两个问题的方案。

var a = 2;
( function foo(){ // <-- 添加这一行
    var a = 3;
    console.log( a ); // 3
})(); // <-- 以及这一行
console.log( a ); // 2
复制代码

使用这个写法,函数会被当作函数表达式,而不是一个标准的函数声明来处理。该写法也被称为自动执行函数表达式。

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

(function foo(){ .. }) 做为函数表达式意味着 foo 只能在 .. 所表明的位置中被访问,外部做用域则不行。 foo 变量名被隐藏在自身中意味着不会非必要地污染外部做用域。

匿名和具名

没有名称标识符的函数表达式,称为匿名函数表达式。反之,有名称标识符的函数表达式,称为具名函数表达式

匿名函数表达式书写起来简单快捷,不少库和工具也倾向鼓励使用这种风格的代码。可是 它也有几个缺点须要考虑。

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

给函数表达式命名是一个最佳实践。

块级做用域

for (var i=0; i<10; i++) {
    console.log( i );
}
复制代码

对于for循环,想必你们都不陌生。

咱们在 for 循环的头部直接定义了变量 i ,一般是由于只想在 for 循环内部的上下文中使用 i ,而忽略了 i 会被绑定在外部做用域(函数或全局)中的事实。这就是块做用域的所带来的好处。而且变量的声明应该距离使用的地方越近越好,并最大限度地本地化。

块做用域是一个用来对以前的最小受权原则进行扩展的工具,将代码从在函数中隐藏信息扩展为在块中隐藏信息。

闭包

闭包是基于词法做用域书写代码时所产生的天然结果,并不须要为了利用它们而有意识的建立闭包。

function foo() {
    var a = 2;
    function bar() {
        console.log( a );
    }
    return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。
复制代码

在上述例子中,bar()函数被正常的执行,可是它是在本身定义的词法做用域外执行的。 在foo()方法执行以后,引擎的垃圾回收器正常状况下会将该方法销毁以释放内存。可是由于闭包的存在,bar()方法调用到了foo()的词法做用域,因此垃圾回收器并无将foo()的内部销毁。

闭包就是在定义的词法做用域之外的地方被调用。

小结

当函数能够记住并访问所在的词法做用域,即便函数是在当前词法做用域以外执行,这时就产生了闭包。 若是没能认出闭包,也不了解它的工做原理,在使用它的过程当中就很容易犯错,好比在循环中。但同时闭包也是一个很是强大的工具,能够用多种形式来实现模块等模式。

结语

做用域的使用在咱们的平常开发中随处可见,灵活的应用和明确的了解本身所写的代码的做用域,能够提到开发的效率。

同时,正确的使用相关知识,也能够提到本身的代码质量。

本篇内容关于闭包的内容较少,主要是由于几个方面:

  • 闭包的概念若是想描述清楚,实属困难。
  • 要验证闭包的种种,须要代入大量的例子和分析,和做用域同一篇章的话,篇幅会过长。

因此本文仅仅只是简单地提到了闭包的一些内容。

但愿个人文章能被大家所喜欢,也但愿如有不足之处,大佬们能一一点出,谢谢。

参考

本文内容,为学习《你不知道的JavaScript》上卷的第一部分【做用域与闭包】后所产出的笔记文章。有兴趣的小伙伴能够直接查看原书籍。

相关文章
相关标签/搜索