list

scope


this

var a = 2分解
- 遇到var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a。
- 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的 变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量(查看 1.3 节)。
如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会举手示意并抛出一个异常!

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

LHS | RHS查询
LHS 查询则是试图找到变量的容器本身,从而可以对其赋值。从这个角度说,RHS 并不是真正意义上的“赋值操作的右侧”,更准确地说是“非左侧”。

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

>LHS 和 RHS 的含义是“赋值操作的左侧或右侧”并不一定意味着就是“= 赋值操作符的左侧或右侧”。赋值操作还有其他几种形式,因此在概念上最 好将其理解为“赋值操作的目标是谁(LHS)”以及“谁是赋值操作的源头
(RHS)”。

1
console.log( a );


其中对 a 的引用是一个 RHS 引用,因为这里 a 并没有赋予任何值。相应地,需要查找并取
得 a 的值,这样才能将值传递给 console.log(..)。

相比之下,例如:
1
a=2;


这里对 a 的引用则是 LHS 引用,因为实际上我们并不关心当前的值是什么,只是想要为 =
2 这个赋值操作找到一个目标。

既有 LHS 也有 RHS 引用
1
2
3
4
function foo(a) { 
console.log( a ); // 2
}
foo( 2 );


>隐式的 a=2 操作发生在 2 被当作参数传递给 foo(..) 函数时,2 会被分配给参数 a。为了给参数 a(隐式地)分配值,需要进行一次 LHS 查询。

>最后一行 foo(..) 函数的调用需要对 foo 进行 RHS 引用,意味着“去找到 foo 的值,并把 它给我”。并且 (..) 意味着 foo 的值需要被执行,因此它最好真的是一个函数类型的值!


小结
作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用 LHS查询;如果目的是获取变量的值,就会使用 RHS查询。

scope


函数中的作用域

3.1 函数中的作用域
>函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复
用(事实上在嵌套的作用域中也可以使用)

3.2 隐藏内部实现
> 最小授权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必要内容,而将其他内容都“隐藏”起来,比如某个模块或对象的 API 设计

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

3.3.1 匿名和具名
匿名函数表达式
1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难
2. 如果没有函数名,当函数需要引用自身时只能使用已经过期的 arguments.callee引用,比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。
3. 匿名函数省略了对于代码可读性 / 可理解性很重要的函数名。一个描述性的名称可以让
代码不言自明
>行内函数表达式非常强大且有用,始终给函数表达式命名是一个最佳实践
1
2
3
setTimeout( function timeoutHandler() { // <-- 快看,我有名字了! 
console.log( "I waited 1 second!" );
}, 1000 );


3.3.2 立即执行函数表达式
>IIFE,代表立即执行函数表达式(Immediately Invoked Function Expression)
1
2
3
4
5
6
7
8
9
var a = 2; 

(function IIFE( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a ); // 2
})( window );

console.log( a ); // 2


3.4 块作用域
1
2
3
4
5
6
7
var foo = true; 

if (foo) {
var bar = foo * 2; // 属于外部作用域
bar = something( bar );
console.log( bar );
}


3.4.2 try/catch
> JavaScript 的 ES3 规范中规定 try/catch 的 catch 分句会创建一个块作
用域,其中声明的变量仅在 catch 内部有效

1
2
3
4
5
6
7
8
try { 
undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
console.log( err ); // 能够正常执行!
}

console.log( err ); // ReferenceError: err not found


3.5 小结
>函数是 JavaScript 中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会
在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。
但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,
也可以属于某个代码块(通常指 { .. } 内部) 。
从 ES3 开始,try/catch 结构在 catch 分句中具有块作用域。
在 ES6 中引入了 let 关键字(var 关键字的表亲) ,用来在任意代码块中声明变量。if
(..) { let a = 2; } 会声明一个劫持了 if 的 { .. } 块的变量,并且将变量添加到这个块
中。
有些人认为块作用域不应该完全作为函数作用域的替代方案。两种功能应该同时存在,开
发者可以并且也应该根据需要选择使用何种作用域,创造可读、可维护的优良代码。

promotion


提升

4.1 先有鸡还是先有蛋
1
2
3
4
5
6
7
8
9
10
11
a = 2; 

var a;

console.log( a ); // 2
// 代码片段会以如下形式进行处理
var a;

a = 2;

console.log( a );


1
2
3
4
5
6
7
8
9
console.log( a ); 

var a = 2; // undefined
// 代码片段实际是按照以下流程处理的:
var a;

console.log( a );

a = 2;


4.2 编译器再度来袭
>这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。这个过程就叫作提升
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
foo(); 

function foo() {
console.log( a ); // undefined
var a = 2;
}
// 理解为下面的形式
function foo() {
var a;

console.log( a ); // undefined

a = 2;
}

foo();

// 可以看到,函数声明会被提升,但是函数表达式却不会被提升。
foo(); // 不是 ReferenceError, 而是 TypeError!

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


4.4 小结
>我们习惯将 var a = 2; 看作一个声明,而实际上 JavaScript 引擎并不这么认为。它将 var a
和 a = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。
这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。
可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的
最顶端,这个过程被称为提升。

>声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升

closure


作用域闭包

如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。 在定时器、事件监听器、Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包。

5.4 循环和闭包
1
2
3
4
5
6
7
for (var i=1; i<=5; i++) { 
(function(j) {
setTimeout( function timer() {
console.log( j );
}, j*1000 );
})( i );
}


>在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

5.5 模块
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
};
}

var foo = CoolModule();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 改进来实现单例模式:
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
};
})();

foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3


this


this全面解析


2.2 this全面解析

2.2.1 默认绑定
>this 的默认绑定,this 指向全局对象

>如果使用严格模式(strict mode) ,那么全局对象将无法使用默认绑定,因此 this 会绑定到 undefined

2.2.2 隐式绑定

1
2
3
4
5
6
7
8
9
10
function foo() {  
console.log( this.a );
}

var obj = {
a: 2,
foo: foo
};

obj.foo(); // 2


隐式丢失
一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式

显式绑定
1
2
3
4
5
6
7
8
9
function foo() {  
console.log( this.a );
}

var obj = {
a:2
};

foo.call( obj ); // 2


>通过 foo.call(..),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。
如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..)new Boolean(..) 或者
new Number(..)) 。这通常被称为“装箱”

硬绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo() {  
console.log( this.a );
}

var obj = {
a:2
};

var bar = function() {
foo.call( obj );
};

bar(); // 2
setTimeout( bar, 100 ); // 2

// 硬绑定的 bar 不可能再修改它的 this
bar.call( window ); // 2


- call, apply, bind

API调用的“上下文”

1
2
3
4
5
6
7
8
9
10
11
function foo(el) {  
console.log( el, this.id );
}

var obj = {
id: "awesome"
};

// 调用 foo(..) 时把 this 绑定到 obj
[1, 2, 3].forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome


>这些函数实际上就是通过 call(..) 或者 apply(..)实现了显式绑定,这样你可以少些一些代码


2.2.4 new绑定
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

1. 创建(或者说构造)一个全新的对象。
2. 这个新对象会被执行 [[ 原型 ]] 连接。
3. 这个新对象会绑定到函数调用的 this。
4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。


1
2
3
4
5
6
function foo(a) {  
this.a = a;
}

var bar = new foo(2);
console.log( bar.a ); // 2


使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定