你不知道的 JS(上)
# 1. 编译原理
编译:分词/词法分析 -> 解析/语法分析 -> 代码生成。
分词/词法分析:将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块称为词法单元。
代码示意 var a = 2; 分析为 var、a、=、2、;
1
2解析/语法分析:将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树(AST)。
可能的代码示意 VariableDeclaration: { Identifier: a, AssignmentExpression: { NumericLiteral: 2 } }
1
2
3
4
5
6
7代码生成:将 AST 转换为可执行代码的过程被称为代码生成。
# 2. LHS 与 RHS
RHS 查询与简单地查找某个变量的值别无二致,而 LHS 查询则是试图找到变量的容器本身,从而可以对其赋值。
最好将其理解为 “赋值操作的目标是谁(LHS)” 以及 “谁是赋值操作的源头(RHS)”。
# 3. 编译器有话说小测验
- 找出所有的 LHS 与 RHS 查询。
function foo(a) {
var b = a
return a + b
}
var c = foo(2)
2
3
4
5
6
- LHS 查询(3 处):c = ..、a = 2(隐式变量分配)、b = ..
- RHS 查询(4 处):foo(2..、= a、a ..、.. b
# 4. 作用域是什么
LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。
不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛出 ReferenceError 异常(严格模式下)。
# 5. 词法作用域与动态作用域
- 主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定。(this 也是! )词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。
function foo() {
console.log(a) // 词法作用域打印2,动态作用域打印3
}
function bar() {
var a = 3
foo()
}
var a = 2
bar()
2
3
4
5
6
7
8
9
10
11
# 6. 欺骗词法
JavaScript 中有两个机制可以 “欺骗” 词法作用域: eval(..) 和 with。前者可以对一段包含一个或多个声明的 “代码” 字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)。后者本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)。
- eval()。
function foo(str, a) { eval(str) console.log(a, b) // 1, 3 } var b = 2 foo('var b = 3', 1)
1
2
3
4
5
6
7- with。
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被泄露到全局作用域上了!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 7. 立即执行函数表达式-IIFE
;(function foo() {
console.log(777)
})()
;(function foo() {
console.log(777)
}())
!function foo() {
console.log(777)
}()
2
3
4
5
6
7
8
9
10
11
# 8. try / catch
- catch 分句会创建一个块作用域,其中的声明的变量仅在 catch 内部有效。
try {
undefined()
} catch (err) {
console.log(err)
}
console.log(err) // ReferenceError: err is not defined
2
3
4
5
6
7
# 9. 闭包
产生原因:我们在词法作用域的环境下写代码,而其中的函数也是值,可以随意传来传去。
产生闭包:当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
function foo() {
var a = 1
// var b = 1
return function bar() {
// debugger // b 被销毁
a++
console.log(a)
}
}
var bar = foo()
bar()
2
3
4
5
6
7
8
9
10
11
12
function wait(message) {
setTimeout(function timer() {
console.log(message)
}, 1000)
}
wait('Hello,closure!')
2
3
4
5
6
7
# 10. 显示绑定
- 如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new String(..)、new Boolean(..)、new Number(..))。这通常被称为 “装箱”。
function foo() {
console.log(this.TheOne)
}
Number.prototype.TheOne = 'NEO'
String.prototype.TheOne = 'NEO'
Boolean.prototype.TheOne = 'NEO'
foo.call(7)
foo.apply('Matrix')
foo.bind(true)
7..TheOne // NEO
'Matrix'.TheOne // NEO
true.TheOne // NEO
2
3
4
5
6
7
8
9
10
11
12
13
# 11. this 绑定规则
- 默认绑定
function foo() {
console.log(this.a) // 2
}
var a = 2
foo()
2
3
4
5
6
- 隐式绑定
function foo() {
console.log(this.a) // 2
}
var obj = {
a: 2,
foo: foo,
}
obj.foo()
2
3
4
5
6
7
8
9
10
- 显示绑定
function foo() {
console.log(this.a) // 2
}
var obj = {
a: 2,
}
foo.call(obj)
2
3
4
5
6
7
8
9
new 绑定。
- 创建(或者说构造)一个全新的对象。
- 这个新对象会被执行[[Prototype]]连接。
- 这个新对象会绑定到函数调用的 this。
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
function Foo(a) {
this.a = a
}
var bar = new Foo(2)
console.log(bar.a) // 2
2
3
4
5
6
# 12. this 全面解析
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置。找到之后就可以顺序应用下面这四条规则来判断 this 的绑定对象。
- 由 new 调用?绑定到新创建的对象。
- 由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
- 由上下文对象调用?绑定到那个上下文对象。
- 默认:在严格模式下绑定到 undefined, 否则绑定到全局对象。
一定要注意,有些调用可能在无意中使用默认绑定规则。如果想 “更安全” 地忽略 this 绑定,你可以使用一个 DMZ 对象,比如 0 = object.create(null),以保护全局对象。
function foo() {
console.log(this.a) // 2
}
var a = 2
foo.call(null)
2
3
4
5
6
- ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这其实和 ES6 之前代码中的 self = this 机制一样。