你不知道的js——做用域javascript
1 做用域java
1.1编译原理node
在传统编译语言的流程中,程序中的一段源代码在执行以前会经历三个步骤,统称为“编 译”jquery
1.2理解做用域程序员
1.3 做用域嵌套ajax
当前做用域没法找到某个变量时,引擎会在外层嵌套的做用域中查找,直至抵达最外层(全局)时仍未找到,抛出错误。编程
1.4 异常api
变量没法找到时,抛出ReferenceError(参考错误),与做用域判别失败相关。 对变量的值进行不合理操做,抛出TypeError,与操做是否合法相关。数组
当引擎执行 LHS 查询时,若是在顶层(全局做用域)中也没法找到目标变量, 全局做用域中就会建立一个具备该名称的变量,并将其返还给引擎,前提是程序运行在非 “严格模式”下。浏览器
(严格模式禁止自动或隐式地建立全局变量。)
(若是 RHS 查询找到了一个变量,可是你尝试对这个变量的值进行不合理的操做, 好比试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的 属性,那么引擎会抛出另一种类型的异常,叫做 TypeError。
ReferenceError 同做用域判别失败相关,而 TypeError 则表明做用域判别成功了,可是对 结果的操做是非法或不合理的。)
2 词法做用域
词法分析阶段 基本可以知道所有标识符在哪里以及是如何声明的,从而可以预测在执行过程当中如何对它 们进行查找。
做用域共有两种主要的工做模型。第一种是最为广泛的,被大多数编程语言所采用的词法 做用域,咱们会对这种做用域进行深刻讨论。另一种叫做动态做用域,仍有一些编程语 言在使用(好比 Bash 脚本、Perl 中的一些模式等)。
2.1 词法阶段
词法做用域即定义在词法阶段的做用域。
查找
做用域气泡的结构和互相之间的位置关系给引擎提供了足够的位置信息,引擎用这些信息 来查找标识符的位置。
做用域查找会在找到第一个匹配的标识符时中止。。在多层的嵌套做用域中能够定义同名的 标识符,这叫做“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。
全局变量会自动成为全局对象(好比浏览器中的 window 对象)的属性,所以 能够间接地经过对全局对象属性的引 用来对其进行访问。——→ window.a
经过这种技术能够访问那些被同名变量所遮蔽的全局变量。但非全局的变量 若是被遮蔽了,不管如何都没法被访问到。
2.2 欺骗词法
在运行时来“修 改”(也能够说欺骗)词法做用域,:欺骗词法做用域会致使性能 降低(缘由主要是没法对代码的词法进行静态分析,预先确认变量和函数的位置,从而快速寻找。)
【另一个不推荐使用 eval(..) 和 with 的缘由是会被严格模式所影响(限 制)。with 被彻底禁止,而在保留核心功能的前提下,间接或非安全地使用 eval(..) 也被禁止了。】
2.2.1 eval
接受一个字符串为参数,并将其中的内容视为好像在书 写时就存在于程序中这个位置的代码
eval(..) 一般被用来执行动态建立的代码
eval(..) 调用中的 "var b = 3;" 这段代码会被看成原本就在那里同样来处理。因为那段代 码声明了一个新的变量 b,所以它对已经存在的 foo(..) 的词法做用域进行了修改。事实 上,和前面提到的原理同样,这段代码实际上在 foo(..) 内部建立了一个变量 b,并遮蔽 了外部(全局)做用域中的同名变量。
在严格模式的程序中,eval(..) 在运行时有其本身的词法做用域,意味着其中的声明没法修改所在的做用域。
与之类似的有setTimeout,setInterval,new Function(),均可以接收字符串。
2.2.2 with
with一般被看成重复引用同一个对象中的多个属性的快捷方式,能够不须要重复引用对象自己。with会建立一个全新的词法做用域。 在严格模式下,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 执行 时,自动建立了一个全局变量(由于是非严格模式)。)
3 函数做用域和块做用域
3.1 函数中的做用域
函数做用域的含义是指,属于这个函数的所有变量均可以在整个函数的范围内使用及复 用(事实上在嵌套的做用域中也可使用)。JS具备基于函数的做用域。内部能向外访问,外部不能向内访问。
3.2 隐藏内部实现
把变量和函数包裹在一个函数的做用域中,而后用这个做用域 来“隐藏”它们。
最小受权或最小暴露原则:在软件设计中,应该最小限度地暴露必要的内容,而将其余内容都隐藏起来。 隐藏做用域可以避免同名标识符之间的冲突。
规避冲突:“隐藏”做用域中的变量和函数所带来的另外一个好处,是能够避免同名标识符之间的冲突, 两个标识符可能具备相同的名字但用途却不同,无心间可能形成命名冲突。冲突会致使 变量的值被意外覆盖。
3.2.1 全局命名空间
引入第三方库时,没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引起冲突
3.2.2 模块管理
另一种避免冲突的办法和现代的模块机制很接近,就是从众多模块管理器中挑选一个来 使用。
无需将标识符加入到全局做用域中,而是经过依赖管理器 的机制将库的标识符显式地导入到另一个特定的做用域中
3.3 函数做用域
若是function是声明中第一个词(前面没有其余词,甚至是括号),那么就是一个函数声明,不然就是一个函数表达式。函数表达式能够是匿名的, 而函数声明则不能够省略函数名
在任意代码片断外部添加包装函数,能够将内部的变量和函数定义“隐 藏”起来,外部做用域没法访问包装函数内部的任何内容。可是它并不理想,由于会致使一些额外的问题。首先, 必须声明一个具名函数 foo(),意味着 foo 这个名称自己“污染”了所在做用域(在这个 例子中是全局做用域)。
函数不须要函数名(或者至少函数名能够不污染所在做用域),而且可以自动运行, 这将会更加理想。
☆☆☆☆☆包装函数的声明以 (function... 而不只是以 function... 开始。,函数会被看成函数表达式而不是一 个标准的函数声明来处理。
(function foo(){ .. }) 做为函数表达式意味着foo 被绑定在函数表达式自身的函数中,外部做用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部做 用域。
3.3.1 匿名和具名
于函数表达式你最熟悉的场景可能就是回调参数
函数表达式能够匿名,函数声明必须具名。 匿名函数也有缺点:
选择性地给函数表达式具名,能够解决以上问题。
行内函数表达式很是强大且有用——匿名和具名之间的区别并不会对这点有任何影响,始终给函数表达式命名是一个最佳实践。
3.3.2 当即执行函数表达式IIFE(Immediately Invoked Function Expression)
用圆括号()将函数包裹,而后紧跟圆括号调用。 另外一种能够将括号放在内部。 (function ( ){ …… } ( )); UMD标准中,IIFE也被普遍运用,好比:
var a = 2; (function IIFE( def ) { def( window ) })(function def ( global ) { var a = 3; console.log( a ); //3 console.log( global.a ); //2 }); //函数表达式 def 定义在片断的第二部分,而后看成参数(这个参数也叫做 def)被传递进 IIFE 函数定义的第一部分中。最后,参数 def(也就是传递进去的函数)被调用,并将 window 传入看成 global 参数的值
3.4 块做用域
在 for 循环的头部直接定义了变量 i,一般是由于只想在 for 循环内部的上下文中使 用 i,而忽略了 i 会被绑定在外部做用域(函数或全局)中的事实。这就是块做用域的用处。变量的声明应该距离使用的地方越近越好,并最大限度地本地 化。
块做用域是一个用来对以前的最小受权原则进行扩展的工具,将代码从在函数中隐藏信息 扩展为在块中隐藏信息。
(因屡次多处访问而提早作缓存除外) 块级做用域在ES6中获得普遍应用。在此以前须要注意,var声明的变量并不属于块级做用域。 能够生成块级做用域的有:
try/catch: catch 分句会建立一个块做 用域,其中声明的变量仅在 catch 内部有效
try {
undefined(); // 执行一个非法操做来强制制造一个异常
} catch (err) {
console.log( err ); // 可以正常执行!
}
console.log( err ); // ReferenceError: err not found
const(ES6新特性),能够造成暂时性锁区。一样能够用来建立块做用域变量,但其值是固定的 (常量)。以后任何试图修改值的操做都会引发错误。
*块级做用域的用处:**有利于垃圾收集。程序块在执行后,其中的变量若是不被后续须要(闭包等),就能够将内存回收 . 减小变量空间污染。
1 垃圾收集
2 let循环
4 提高
4.1 先有鸡仍是先有蛋
function和var声明,会被提高到顶部。 其余的操做不会提高,处于自身本来的位置。
能够看到,函数声明会被提高,可是函数表达式却不会被提高。 foo(); // 不是 ReferenceError, 而是 TypeError! var foo = function bar() { // ... }; //这段程序中的变量标识符 foo() 被提高并分配给所在做用域(在这里是全局做用域),所以 foo() 不会致使 ReferenceError。可是 foo 此时并无赋值(若是它是一个函数声明而不 是函数表达式,那么就会赋值)。foo() 因为对 undefined 值进行函数调用而致使非法操做, 所以抛出 TypeError 异常。
4.2 编译器再度来袭
当你看到 var a = 2; 时,可能会认为这是一个声明。但 JavaScript 实际上会将其当作两个声明:var a; 和 a = 2; 第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在 原地等待执行阶段。
每一个做用域都会进行提高操做,其顶部为其自身做用域的顶部(注意不会跨越做用域)。 函数表达式不会提高,而是处于正常的位置。 具名的函数表达式也不存在提高
foo(); // TypeError,由于此时foo被初始化为undefined,尚未赋值为函数表达式 bar(); // ReferenceError,由于函数表达式不提高,此时bar尚未声明 var foo = function bar(){ // do sth }
等效于
var foo; foo(); // TypeError,由于此时foo被初始化为undefined,尚未赋值为函数表达式 bar(); // ReferenceError,由于函数表达式不提高,此时bar尚未声明 foo = function (){ var bar = self; // do sth }
4.3 函数优先
函数首先被提高,而后是变量。函数与变量同名时,变量的声明会被省略,可是依旧能够进行赋值操做。 后出现的函数声明会覆盖前面的同名函数,因此在同一做用域内,千万不要声明同名的函数 在普通块内部的声明,也会提高到做用域顶部,而不是在块内。
//一个普通块内部的函数声明一般会被提高到所在做用域的顶部,这个过程不会像下面的代 码暗示的那样能够被条件判断所控制: foo() // 'b' var a = true; if(a){ function foo(){ console.log('a') } // 被提高到做用域顶部。先声明,被后面覆盖了 } else { function foo(){ console.log('b') } // 被提高到第二个。后声明,覆盖了前面 }
所以应该 尽量避免在块内部声明函数。
5 做用域闭包
5.1 启示
JS中闭包无处不在。 闭包是基于词法做用域书写代码时产生的天然结果。 闭包特征就是将函数表达式,连带其词法做用域,进行传值。
5.2 实质问题
一个闭包的例子:
function foo(){ var a = 2; function bar(){ console.log(a); } return bar; //也能够直接返回函数表达式,function(){ console.log(a); } } var baz = foo(); baz(); // 2,读取到了foo中定义的变量a
本质上是,函数bar的词法做用域可以访问foo的内部做用域,由于它的做用域嵌套在了foo的做用域内。 也就是说,最终return的结果是带有scope(做用域)信息的,这个是可以找到须要调用变量的根本依据。 当bar在使用时,闭包会阻止垃圾回收器回收foo的做用域(有好有坏)。
5.3 如今我懂了
引擎在调用函数的同时,其词法做用域会保持完整。 若是将(访问他们各自词法做用域的)函数看成第一级的值类型并处处传递,你就会看到闭包在这些函数中的应用。 也就是说,只要使用了回调函数,实际上就是在使用闭包。(在定时器、事件监听器、 Ajax 请求、跨窗口通讯、Web Workers 或者任何其余的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!)
5.4 循环和闭包
异步时候,函数进行回调时,调用变量值的是回调发生时候的值,因此要考虑运行时的状态。
拓展解析:任务队列、进阶
for(var i=0; i<5; i++){ setTimeout( function(){ console.log(i); //输出都是5 }, 1000*i); }
解决方法有IIFE
for(var i=0; i<5; i++){ // 建立一个新的词法做用域,把它的值在这个做用域里面记录下来 (function(j){ setTimeout(function(){ console.log(j); //这样输出就是0,1,2,3,4了 }, 1000*j); })(i); }
或者利用做用块
// let能够做用于做用块 for(let i=0; i<5; i++){ setTimeout( function(){ console.log(i); //这样输出就是0,1,2,3,4了 }, 1000*i); }
这里面本质就是
for(var i=0; i<5; i++){ // 使用仅存在于该做用块的变量 let _i = i; setTimeout( function(){ console.log(_i); //这样输出就是0,1,2,3,4了 }, 1000*_i); }
5.5 模块
实现模块的模式称为模块暴露。
function MyModule(){
var something = 'cool';
var another = [1,2,3];
function doSomething(){ console.log(something); } function doAnother(){ console.log(another.join(',')); } //ES5的写法 return { doSomething: doSomething, doAnother: doAnother, }; // ES6的写法 /* return { doSomething, doAnother, }; */
}
// 获得了这个闭包
var foo = MyModule();
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 }; return publicAPI; })( "foo module" ); foo.identify(); // foo module foo.change(); foo.identify(); // FOO MODULE
经过在模块实例的内部保留对公共 API 对象的内部引用,能够从内部对模块实例进行修 改,包括添加或删除方法和属性,以及修改它们的值。
经过在模块实例的内部保留对公共 API 对象的内部引用,能够从内部对模块实例进行修 改,包括添加或删除方法和属性,以及修改它们的值。
5.5.1 现代的模块机制
一个典型的模块管理器能够定义为(应该是参考了Require.js)
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, }; });
其使用方式为
// 名称为bar的模块,没有依赖 MyModules.define('bar', [], function(){ function hello(who){ return 'Let me introduce: ' + who; } return { hello: hello, }; }); // 名称为foo,依赖了bar。 // 注意这里函数表达式能拿到bar,是由于define方法去modules中抽取了bar,而后传入给它。 MyModules.define('foo', ['bar'], function(bar){ var hungry = 'hippo'; function awesome(){ // 由于外层参数已经传入了bar,因此这里也就能拿到bar的hello方法 console.log(bar.hello(hungry).toUpperCase()); } return { awesome: awesome, }; }); var foo = MyModules.get('foo'); foo.awesome(); // LET ME INTRODUCE HIPPO
5.5.2 将来的模块机制
ES6 的模块没有“行内”格式,必须被定义在独立的文件中(一个文件一个模块)。浏览 器或引擎有一个默认的“模块加载器”(能够被重载,但这远超出了咱们的讨论范围)可 以在导入模块时异步地加载模块文件。
bar.js
export function hello(who){ return 'Let me introduce: ' + who; }
foo.js
// 直接得到了hello import {hello} from 'bar'; var hungry = 'hippo'; export function awesome(){ console.log(hello(hungry).toUpperCase()); }
baz.js
// 导入完整的foo模块 module foo from 'bar'; foo.awesome(); // LET ME INTRODUCE HIPPO
5.6 小结
闭包相似于一个标准,关于如何在函数做为值按需传递的词法环境中书写代码的
当函数能够记住并访问所在的词法做用域,即便函数是在当前词法做用域以外执行,这时 就产生了闭包。
时闭包也是一个很是强大的工具,能够用多种形式来实现模块等模式。
模块有两个特征:
补充:模块机制、AMD、require.js
当即执行函数写法
使用"当即执行函数"(Immediately-Invoked Function Expression,IIFE),能够达到不暴露私有成员的目的。
var module1 = (function(){ var _count = 0; var m1 = function(){ //... }; var m2 = function(){ //... }; return { m1 : m1, m2 : m2 }; })();
使用上面的写法,外部代码没法读取内部的_count变量。
console.info(module1._count); //undefined
module1就是Javascript模块的基本写法。下面,再对这种写法进行加工。
放大模式:,必须分红几个部分,或者一个模块须要继承另外一个模块,这时就有必要采用"放大模式"(augmentation)。
var module1 = (function (mod){ mod.m3 = function () { //... }; return mod; })(module1);
上面的代码为module1模块添加了一个新方法m3(),而后返回新的module1模块。
宽放大模式(Loose augmentation) :在浏览器环境中,模块的各个部分一般都是从网上获取的,有时没法知道哪一个部分会先加载。若是采用上一节的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"。
var module1 = ( function (mod){ //... return mod; })(window.module1 || {});
与"放大模式"相比,"宽放大模式"就是"当即执行函数"的参数能够是空对象。
输入全局变量
独立性是模块的重要特色,模块内部最好不与程序的其余部分直接交互。
为了在模块内部调用全局变量,必须显式地将其余变量输入模块。
var module1 = (function ($, YAHOO) { //... })(jQuery, YAHOO);
上面的module1模块须要使用jQuery库和YUI库,就把这两个库(实际上是两个模块)看成参数输入module1。这样作除了保证模块的独立性,还使得模块之间的依赖关系变得明显。
AMD规范
通行的Javascript模块规范共有两种:CommonJS和AMD。
CommonJS
2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。
node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就能够像下面这样加载。
var math = require('math');
而后,就能够调用模块提供的方法:
var math = require('math');
math.add(2,3); // 5
由于这个系列主要针对浏览器编程,不涉及node.js,因此对CommonJS就很少作介绍了。咱们在这里只要知道,require()用于加载模块就好了。
浏览器环境 对于浏览器,模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
所以,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。
AMD也采用require()语句加载模块,可是不一样于CommonJS,它要求两个参数:
require([module], callback);
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功以后的回调函数。
目前,主要有两个Javascript库实现了AMD规范:require.js和curl.js。
require.js的用法
依次加载多个js文件会有很大的弊端。
首先,加载的时候,浏览器会中止网页渲染,加载文件越多,网页失去响应的时间就会越长;其次,因为js文件之间存在依赖关系,所以必须严格保证加载顺序(好比上例的1.js要在2.js的前面),依赖性最大的模块必定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难。
require.js的诞生解决了两个问题
require.js的加载
使用require.js的第一步,是先去官方网站下载最新版本。
下载后,假定把它放在js子目录下面,就能够加载了。
<script src="js/require.js"></script> //加载这个文件,也可能形成网页失去响应。解决办法有两个,一个是把它放在网页底部加载,另外一个是写成下面这样: <script src="js/require.js" defer async="true" ></script> //async属性代表这个文件须要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer,因此把defer也写上。 /*加载require.js之后,下一步就要加载咱们本身的代码了。假定咱们本身的代码文件是main.js,也放在js目录下面。那么,只须要写成下面这样就好了:*/ <script src="js/require.js" data-main="js/main"></script> //data-main属性的做用是,指定网页程序的主模块。在上例中,就是js目录下面的main.js,这个文件会第一个被require.js加载。因为require.js默认的文件后缀名是js,因此能够把main.js简写成main。
主模块的写法
这时就要使用AMD规范定义的的require()函数。
require()异步加载moduleA,moduleB和moduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。
一个实际的例子:假定主模块依赖jquery、underscore和backbone这三个模块,main.js就能够这样写:
require(['jquery', 'underscore', 'backbone'], function ($, _, Backbone){ // some code here }); //require.js会先加载jQuery、underscore和backbone,而后再运行回调函数。主模块的代码就写在回调函数中。
模块的加载
默认状况下,require.js假定这三个模块与main.js在同一个目录,文件名分别为jquery.js,underscore.js和backbone.js,而后自动加载。
使用require.config()方法,咱们能够对模块的加载行为进行自定义。require.config()就写在主模块(main.js)的头部。参数就是一个对象,这个对象的paths属性指定各个模块的加载路径。
require.config({ paths: { "jquery": "jquery.min", "underscore": "underscore.min", "backbone": "backbone.min" } });
若是这些模块在其余目录,好比js/lib目录,则有两种写法。一种是逐一指定路径;另外一种则是直接改变基目录(baseUrl) 。
require.config({//1 paths: { "jquery": "**lib/**jquery.min", "underscore": "**lib/**underscore.min", "backbone": "**lib/**backbone.min" } }); //2 require.config({ baseUrl: "js/lib", paths: { "jquery": "jquery.min", "underscore": "underscore.min", "backbone": "backbone.min" } }); //也能够直接指定它的网址 require.config({ paths: { "jquery": "https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min" } });
AMD模块的写法
模块必须采用特定的define()函数来定义。若是一个模块不依赖其余模块,那么能够直接定义在define()函数之中。
假定如今有一个math.js文件,它定义了一个math模块。那么,math.js就要这样写:
define(function (){ var add = function (x,y){ return x+y; }; return { add: add }; }); //加载方法以下: require(['math'], function (math){ alert(math.add(1,1)); });
若是这个模块还依赖其余模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性。
define(['myLib'], function(myLib){ function foo(){ myLib.doSomething(); } return { foo : foo }; });//当require()函数加载上面这个模块的时候,就会先加载myLib.js文件。