《你不知道的JavaScript》第一部分:作用域和闭包
第1章 作用域是什么
抛出问题:程序中的变量存储在哪里?程序需要时,如何找到它们?
设计 作用域
的目的:为了更好地存储和访问变量。
作用域
:根据名称查找变量的一套规则,用于确定在何处以及如何查找变量(标识符)。
☞ 编译原理
JavaScript 是一门编译语言,但它不是 提前编译的,编译结果也不能在分布式系统中进行移植。
程序的源代码在执行前的三个步骤,统称为“编译”:
- 分词/词法分析:将字符串分解成代码块(词法单元)
- 解析/语法分析:将词法单元流(数组)转换成抽象语法树(Abstract Syntax Tree, AST)
- 代码生成:将AST转换成可执行代码
JavaScript 引擎不会有大量的(像其他语言编译器那么多的)时间用来进行优化。JavaScript 的编译不是发生在构建之前,而是发生在代码执行前的几微秒,甚至更短。因此,JavaScript 引擎用尽了各种方法来保证新更能最佳。
☞ 理解作用域
引擎
从头到尾负责整个JavaScript程序的编译及执行过程;
编译器
负责语法分析及代码生成;
作用域
负责收集并维护所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
总结:变量的赋值操作会执行两个动作——>
1)编译器会在当前作用域中声明一个变量(如果之前没有声明过);
2)在运行时,引擎会在作用域中查找该变量,如果能够找到就会对它赋值。
引擎的LHS查询和RHS查询
LHS查询:赋值操作的左侧,试图找到变量的容器本身,从而可以对其赋值;
// 不关心当前的值是什么,只是想要为 =2 这个赋值操作找到一个目标
a = 2;
不成功的 LHS引用 会导致自动隐式创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛出 ReferenceError 异常。
RHS查询:赋值操作的右侧,查找某个变量的值。
// a 并没有赋予任何值
// 需要查找并取得 a 的值,这样才能传递给 console.log(...)
console.log( a );
不成功的 RHS 查询引用会抛出 ReferenceError 异常。
嵌套作用域
LHS 查询和 RHS 查询都会在当前执行作用域中开始,如果没有找到,就会向上级作用域继续查找目标标识符,直至抵达全局作用域,便停止。
第2章 词法作用域
作用域的两种工作模型:
- 词法作用域:定义在词法阶段的作用域,即,是由你在写代码时将变量和块作用域写在哪里来决定的。
- 动态作用域:在运行时确定,关注函数从何处调用——>this。
作用域气泡由其对应的作用域块代码写在哪里决定,它们是逐级包含的。
☞ 查找
作用域气泡的结构和互相之间的位置关系给引擎提供了足够的位置信息,引擎用这些信息来查找标识符的位置。
☞ 欺骗词法
—— 在运行期修改书写期的词法作用域,会导致性能下降,不要使用。
(1) eval: 接受一个字符串作为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。会修改词法作用域。
function foo(str, a) {
eval( str ); // 欺骗!
console.log(a, b);
}
var b = 2;
foo( 'var b = 3;', 1); // 1,3
在严格模式中,无法修改所在的作用域。
(1) with: 重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。会重新创建一个全新的词法作用域。
在严格模式中,with 被完全禁止。
第3章 函数作用域和块作用域
☞ 函数作用域
函数作用域:指属于这个函数的全部变量都可以在整个函数的范围内使用及复用(嵌套作用域也可以)。
最小特权原则(最小授权/最小暴露原则)
指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的API设计。
隐藏作用域的好处:
- 遵循最小特权原则,避免暴露过多的变量和函数;
- 避免同名标识符之间的冲突。
规避冲突的方式:1)全局命名空间;2)模块管理。
函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。前者会绑定在所在作用域中,而后者会绑定在函数表达式自身的函数中而不是所在作用域中。
行内函数表达式 可以解决匿名函数表达式的缺点。始终给函数表达式命名是一个最佳实践。
setTimeout(function timeoutHandler() {
//...
}, 1000);
☞ 立即执行函数表达式
具名函数的IIFE(立即执行函数表达式)具有匿名函数表达式的所有优势,也是值得推广的实践。
IIFE 的进阶用法:把它们当作函数调用并传递参数进去。
var a = 2;
(function IIFE(global) {
var a = 3;
console.log(a); //3
console.log(global.a); // 2
})(window); console.log(a); // 2
这个模式的另一个应用场景:解决 `undefined 标识符的默认值被错误覆盖导致的异常(虽然不常见)
undefined = true; // 反模式,绝对不要这么做! (function IIFE(undefined) {
var a;
if (a === undefined) {
console.log('undefined is safe here!');
}
})();
IIFE 还有一种变化的用途:倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当做参数传递进去。
var a = 2;
(function IIFE(def) {
def(window);
})(function def(global) { var a = 3;
console.log(a); // 3;
console.log(global.a); // 2
});
☞ 块作用域
块作用域的好处:变量的声明应该距离使用的地方越近越好,并最大限度地本地化。
块作用域的例子:with、try/catch、let、const
使用
let
进行的声明不会在块作用域中进行提升。
第4章 提升
引擎会在解释 JavaScript 代码之前首先对其进行编译。编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。
包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。这个过程就好像所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。
[例子]
// 我们习惯将它看作一个声明
var a = 2;
/**
* JavaScript 引擎会把它当作两个单独的声明
*/
var a; // 定义声明,在编译阶段进行
a = 2; // 赋值声明,在执行阶段进行
每个作用域都会进行提升 操作。但,函数声明会被提升,函数表达式不会被提升。
避免在块内部声明函数。
第5章 作用域闭包
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
// 实际上只是通过不同的标识符引用调用了内部的函数 bar()
baz(); // 2 —— 闭包的效果
由于 bar() 声明的位置,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后的任何时间进行引用。
bar() 依然持有对该作用域的引用,这个引用就叫作闭包。
闭包的作用:阻止垃圾回收器回收内部作用域,使得函数在定义时的词法作用域以外的地方被调用时,仍然可以继续访问定义时的词法作用域。
[闭包示例2:]
function foo() {
var a = 2;
function baz() {
console.log(a);
}
bar(baz);
}
function bar(fn) {
fn(); // 闭包
}
foo(); // 2
[闭包示例3:]
var fn;
function foo() {
var a = 2;
function baz() {
console.log(a);
}
fn = baz; // 将 baz 分配给全局变量
}
function bar() {
fn(); // 闭包
}
foo();
bar(); // 2
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
☞ 模块——利用闭包实现
最常见的实现模块模式的方法通常被成为 模块暴露
:
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
}
}
/**
* 必须通过调用 CoolModule() 来创建一个模块实例
* 如果不执行外部函数,内部作用域和闭包都无法被创建
*/
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1!2!3
模块模式需要具备两个条件:
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
一个具有函数属性的对象并不是真正的模块;
一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。
[单例模式:当只需要一个实例时]:
var foo = (function CoolModule() {
// ...
})();
foo.doSomething(); // cool
foo.doAnother(); // 1!2!3
[模块模式的变化用法:命名将要作为公共API返回的对象]:
var foo = (function CoolModule(id) {
function change() {
// 修改公共API
publicAPI.identify = identify2;
}
function identify1() {
console.log(id);
}
function identify2() {
console.log(id.toUpperCase());
}
var publicAPI = {
change: change,
identify: identify1
}
})('foo module');
foo.identify(); // 'foo module'
foo.change();
foo.identify(); // 'FOO MODULE'
通过在模块内部保留对公共API对象的内部引用,可以从内部对模块实例进行修改,包括添加或删除方法和属性,以及修改它们的值。
☞ 现代的模块机制(没太看明白)
大多数模块依赖加载器/管理器本质上都是将这种模块定义封装进一个友好的 API。
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i=0; i<deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply(impl, deps);
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();
// 定义模块
MyModules.define('bar', [], function() {
function hello(who) {
return 'Let me introduce: ' + who;
}
return {
hello: hello
};
});
MyModules.define('foo', ['bar'], function() {
var hungry = 'hippo';
function awesome() {
console.log(bar.hello(hungry).toUpperCase());
}
return {
awesome: awesome
};
});
var bar = MyModules.get('bar');
var foo = MyModules.get('foo');
console.log(
bar.hello('hippo')
); // Let me introduce: hippo
foo.awesome();
☞ 未来的模块机制
ES6 的模块没有“行内”格式,必须被定义在独立的文件中(一个文件一个模块)。浏览器或引擎有一个默认的“模块加载器”可以在导入模块时异步地加载模块文件。
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
- import:将一个模块中的一个或多个API导入到当前作用域中,并分别绑定在一个变量上;
- module:将整个模块的API导入并绑定到一个变量上;
- export:将当前模块的一个标识符(变量、函数)导出为公共API。
《你不知道的JavaScript》第一部分:作用域和闭包的更多相关文章
- javascript(面向对象,作用域,闭包,设计模式等)
javascript(面向对象,作用域,闭包,设计模式等) 1. 常用js类定义的方法有哪些? 参考答案:主要有构造函数原型和对象创建两种方法.原型法是通用老方法,对象创建是ES5推荐使用的方法.目前 ...
- JavaScript中的作用域和闭包
首先强烈安利<你不知道的JavaScript>,JS初学者进阶必读. 对于从C++.Java等静态语言转向JavaScript的初学者(比如我)来说,JS一些与众不同而又十分要紧的特性使得 ...
- javascript 函数和作用域(闭包、作用域)(七)
一.闭包 JavaScript中允许嵌套函数,允许函数用作数据(可以把函数赋值给变量,存储在对象属性中,存储在数组元素中),并且使用词法作用域,这些因素相互交互,创造了惊人的,强大的闭包效果.[upd ...
- 你不知道的js异步、作用域、闭包
例题如下: for (var i = 0; i < 3; i++) { setTimeout(function() { console.log(i); }, 0) ...
- 你不知道的JavaScript——第一章:作用域是什么?
编译原理 JavaScript事实上是一门编译语言,但与传统的编译语言不同,它不是提前编译的,编译结果也不能在分布式系统中进行移植. 任何JavaScript代码片段在执行前都要进行编译(通常就在执行 ...
- JavaScript概念总结:作用域、闭包、对象与原型链
1 JavaScript变量作用域 1.1 函数作用域 没有块作用域:即作用域不是以{}包围的,其作用域完成由函数来决定,因而if /for等语句中的花括号不是独立的作用域. 如前述,JS的在函数中定 ...
- Javascript深入之作用域与闭包
相信绝大多数同学都听过闭包这个概念,但闭包具体是什么估计很少有人能够说的很详细.说实话闭包在我们平时开发中应该是很常见的,并且在前端面试中闭包也是常见的重要考点,在学习闭包之前我们先来看看作用域与作用 ...
- Javascript中关于作用域和闭包和域解释的面试题
<script type="text/javascript"> function fn() { var i = 10; return function (n) { co ...
- 你不知道的JavaScript(上)作用域与闭包
第一部分 作用域与闭包 第一章 作用域是什么 1.作用域 变量赋值操作会执行两个动作:首先编译器会在当前作用域中声明一个变量(如果之前没有声明过), 然后会在运行时引擎会在作用域中查找该变量,找到就会 ...
- javascript作用域和闭包之我见
javascript作用域和闭包之我见 看了<你不知道的JavaScript(上卷)>的第一部分--作用域和闭包,感受颇深,遂写一篇读书笔记加深印象.路过的大牛欢迎指点,对这方面不懂的同学 ...
随机推荐
- Oozie简介
在Hadoop中执行的任务有时候需要把多个Map/Reduce作业连接到一起,这样才能够达到目的.[1]在Hadoop生态圈中,有一种相对比较新的组件叫做Oozie[2],它让我们可以把多个Map/R ...
- WordPress的神器
导读 Markdown 是一种简单的标记语言,旨在帮助你花费更小的代价来格式化纯文本文档.在 WordPress 下你可以使用 HTML 或者可视化编辑器来格式化你的文档,但是使用 markdown ...
- [原创]checkstyle下载与安装
checkstyle是一款功能很强的java静态代码检查工具,为eclipse的插件.在网上看了,大致有两种安装方法.第一种 为联网,在eclipse里输入URL下载:另一种为下载好插件后,离线安装. ...
- 两个activity之间传递数据用startActivityForResult方法。
package com.example.testactivityresquest; import android.app.Activity; import android.content.Intent ...
- OL/SQL编程练习
create or replace procedure pr_first is --一个变量 v_a ) := '总有一天我的生命将走到尽头'; --一个常量 c_b constant ) := '而 ...
- Hibernate中的一级缓存、二级缓存和懒加载
1.为什么使用缓存 hibernate使用缓存减少对数据库的访问次数,从而提升hibernate的执行效率.hibernate中有两种类型的缓存:一级缓存和二级缓存. 2.一级缓存 Hibenate中 ...
- struts中的请求数据自动封装
Struts 2框架会将表单的参数以同名的方式设置给对应Action的属性中.该工作主要是由Parameters拦截器做的.而该拦截器中已经自动的实现了String到基本数据类型之间的转换工作.在st ...
- C#操作Access数据库(创建&修改结构)
本文转自:http://www.cnblogs.com/liyugang/archive/2012/11/17/2775393.html 想要在程序中控制Access,不是数据,而是Access数据库 ...
- bitset常用函数用法记录 (转载)
有些程序要处理二进制位的有序集,每个位可能包含的是0(关)或1(开)的值.位是用来保存一组项或条件的yes/no信息(有时也称标志)的简洁方法.标准库提供了bitset类使得处理位集合更容易一些.要使 ...
- matlab 2012 vs2010混合编程
电脑配置: 操作系统:window 8.1 Matlab 2012a安装路径:D:\Program Files\MATLAB\R2012a VS2010 : OpenCV 2.4.3:D:\Progr ...