作用域
LHS,查找的目的是对变量进行赋值。
RHS,查找的目的是获取变量的值。
LHS 和 RHS 查询都会在当前作用域中开始,如果没有就继续向上一层查询,一直到最顶部(全局作用域)
- RHS,找不到所需要的变量,会抛出 ReferenceError, RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作,比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError
- LHS,如果找不到需要的变量,在严格模式下”use strict“; 会抛出ReferenceError,在非严格模式下会在最顶层的作用域中创建该变量。
- ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对结果的操作是非法或不合理的
小测验答案
function foo(a) {
var b = a;
return a + b;
}
var c = foo( 2 );
- 找出所有的 LHS 查询(这里有 3 处!)
c = ..; 、 a = 2 (隐式变量分配)、 b = .. - 找出所有的 RHS 查询(这里有 4 处!)
foo(2.. 、 = a; 、 a .. 、 .. b
词法作用域
词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。
- 标识符的查找是从最内部作用域开始的,逐级向上,直到遇到第一个为止。
- 全局变量会自动成为全局对象,类型与 window.a
- 无论函数在哪里被调用,也无论如何调用,它的词法作用域都只由函数被声明时所处的位置决定。
欺骗词法
欺骗词法会导致性能下降,因为编译器不知道里面是什么东西,无法提前优化。
这里有两个词法,eval(...),with(...) 其中,eval见过几次没有深究,with就没有见过,在严格模式下这两个用法都会有问题,再使用时不推荐使用。
function foo(str, a) {
eval( str ); // 欺骗!
console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3
在严格模式的程序中, eval(..) 在运行时有其自己的词法作用域,
function foo(str) {
"use strict";
eval( str );
console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );
setTimeout(..)
setInterval(..)
这些都不推荐使用。
with(),通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。严格模式下,with被完全禁止
使用范例如下
var obj = {
a: 1,
b: 2,
c: 3
};
// 单调乏味的重复 "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
// 简单的快捷方式
with (obj) {
a = 3;
b = 4;
c = 5;
}
在非严格模式下会出现下面的问题。
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!
函数作用域
- 函数的作用域可以隐藏内部实现,只能通过函数来访问,这叫做最小授权原则,这也是许多类库的做法。
- 函数的作用域还通常 用来规避冲突,避免同名标识符之间的冲突。
举例说明
function foo() {
function bar(a) {
i = 3; // 修改 for 循环所属作用域中的 i ,如果避免这种情况,可以使用 var i = 3 ,或者换个变量。
console.log( a + i );
}
for (var i=0; i<10; i++) {
bar( i * 2 ); // 糟糕,无限循环了!
}
}
foo();
库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作库的命名空间,所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴漏在顶级的词法作用域中。
包装函数的声明以 (function... 而不仅是以 function... 开始。尽管看上去这并不是一个很显眼的细节,但实际上却是非常重要的区别。函数会被当作函数表达式而不是一个标准的函数声明来处理。
区分函数声明和表达式最简单的方法是看 function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。
- 函数声明,会被绑在所在的作用域中。
- 函数表达式,会被绑定在自身函数中。
立即执行表达式IIFE
Immediately Invoked Function Expression
两种表达形式
- (function(){ .. }())
- (function(){ .. })();
var a = 2;
(function foo() {
var a = 3;
console.log( a ); // 3
})();
console.log( a ); // 2
进阶,传个参数进去
var a = 2;
(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );
块作用域
JavaScript 没有特定的块作用域,有几个特殊的,
- try/catch中catch{}所包的块。
- let 定义的变量,会默认搞一个块,ES6支持
- const 定义的常量
特别说明 var 定义的变量,虽然是写在块中,其实是定义在了最顶层的作用域中。这里所说的是代码块,并不是函数,函数内var定义的是据测试,还是跑不到顶层去的。
for(var i = 0 ; i< 2 ; i++){
var bar = "a";
}
console.log(i)//2
console.log(bar);//a
function test(){
var t = "t";
}
test();
console.log(t);//ReferenceError t
先赋值还是先声明
在js中,编译器动作是两步,1,编译,2,执行
所以代码中的定义声明部分在步骤1编译的时候就执行完毕了,赋值等操作会被留在原理等待执行阶段再执行。这个过程就好象是声明过程被提前执行了,也成为提升。
- 函数内部的声明在函数内部提升,不会提升到函数的外部。
- 函数声明会被提升,函数表达式不会被提升。,即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() {
// ...
};
- 函数声明优先于变量声明,提升。
- 相同的声明,后面声明会覆盖前面的声明。
闭包
闭包,这是个神秘的东西,听了许多遍,依然不知到它是什么意思,直到这里。
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包!
for (var i=1; i<=5; i++) {
(function() {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
})();
}
//输出,每隔1秒输出一个6,因为输出的都是相同的词法作用域中的i
for (var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})();
}
//这个可以输出 1,2,3,4,5 因为有 j存储 i的值
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}
//这个也可以,在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
for (let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
//这个也可以,for 循环头部的 let 声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。
模块
模块模式必须要具备的两个条件
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
- 封闭函数必须至少返回一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
** 一个具有函数属性的对象本身并不是真正的模块。从方便观察的角度看,一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块。**
模块模式另一个简单但强大的变化用法是,命名将要作为公共 API 返回的对象
现代的模块机制
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i=0; i
这里的重点是 modules[name] = impl.apply( impl, deps );
其中 apply 在各个js库中经常见到,到底是什么意思呢?
在 javascript 中,call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向
未来的模块机制
ES6 中为模块增加了一级语法支持。但通过模块系统进行加载时,ES6 会将文件当作独立的模块来处理。每个模块都可以导入其他模块或特定的API 成员,同样也可以导出自己的API 成员。