你不知道的JS-作用域

作用域

1.1编译原理

  • 分词/词法分析(Tokenizing/Lexing)
    将字符串分解成代码块(词法单元)
    分词和词法分析的差异在于词法单元的识别是通过有状态还是无状态的
  • 解析/语法分析(Parsing)
    将词法单元流(数组)转换为由元素逐级嵌套组成的代表程序语法结构的树(抽象语法树AST)
  • 代码生成
    将AST转换为可执行代码

1.2作用域

JS运行

  • 引擎:负责JS编译与执行
  • 编译器:负责语法分析以及代码生成
  • 作用域:收集并维护所有声明的变量(标识符),根据规则确保执行代码对变量的访问权限

声明解析

1
var a = 2
1. `var a`编译器查询作用域是否存在变量`a`,有则忽略继续编译,无则要求作用域在当前作用域集合中声明一个为`a`的变量
2. 编译器生成运行代码给引擎,处理`a = 2`赋值操作。引擎询问作用域,当前作用域是否存在`a`,存在就使用,否则继续查找。
   最终没有查找到将会报错。

LHS/RHS

变量出现在赋值操作左侧进行LHS,非左侧进行RHS

LHS:只查找
BHS:查找并取到值()

1
2
3
4
5
6
7
8
function foo(a) {
console.log(a)
//RHS,获得a的值传入console.log方法
//RHS,获得console对象,并得到log方法
}
foo(2)
//函数调用RHS,查找foo并获得foo的值
//LHS查询,将2赋值给参数a

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(a) {
var b = a
//LHS,b=
//RHS,=a
return a + b
//RHS, a
//RHS, b
}
var c = foo(2)
//LHS,c=
//RHS,foo(2)
//LHS,(2)
//LHS,foo

1.3作用域嵌套

作用域嵌套,在当前作用域无法找到变量时,引擎会在外层作用域中继续查找

1.4异常

RHS查询找不到会抛出异常
非严格LHS找不到会创建一个
严格模式LHS找不到也会抛出异常
RHS进行不合理操作会抛出TypeError

第二章:词法作用域

两种工作模型

  1. 词法作用域
  2. 动态作用域

    2.1词法阶段

    定义在词法阶段的作用域。(写代码是将变量和块作用域写在哪里决定的)

查找:

遮蔽效应:内部标识符”遮蔽”外部标识符

2.2欺骗词法

欺骗词法作用域会导致性能下降

2.2.1 eval

eval中包含一个或多个声明会对作用域进行修改
严格模式下,eval有自己的词法作用域

setTimeout(),setInterval()第一个参数是字符串的时候,字符串可以被解释为动态生成的函数代码

new Function()接受代码字符串,转换为动态生成的函数

避免使用以上方式

2.2.2 with

严格模式下被禁止

2.2.3 性能

在编译阶段进行的性能优化,有些依赖于能够根据代码词法进行静态分析,预先确定所有变量和函数的定义位置,才能执行时快速找到标识符。

第三章,函数作用域和块作作用域

3.1 函数中的作用域

Js基于函数的作用域
含义:属于这个函数的全部变量都可以在整个函数范围内使用以及复用

3.2 隐藏内部实现

最小授权,最小暴露原则:最小限度的暴露必要内容

规避冲突:避免同名标识符之间的冲突

  1. 全局命名空间
    通过全局作用域生命对象,将功能通过对象暴露给外界
  2. 模块管理

3.3 函数作用域

让函数名不污染所在作用域,并能够自动运行
函数表达式写法
如果function 是声明的第一个词,就是函数声明,否则是函数表达式
函数声明和函数表达式的区别:名称标识符绑定在何处
函数声明会绑定在所在作用域中
函数表达式会绑定在函数表达式自身的函数中

3.3.1 匿名和具名

匿名函数的缺点:

1. 调试困难
2. 引用自身需要`arguments.callee`
3. 可读性差
行内函数表达式可以指定函数名,所以给函数表达式命名是最佳实践

3.3.2 立即执行函数表达式IIFE

IIFE的两种写法

  1. (function(){})()
  2. (function(){}())

IIFE的用途

  1. 函数调用传参
    1
    2
    3
    (function(global){})(window)
    (function(undefined){})()
    可以确保undefined是undefined

2.倒置代码运行顺序
将需要运行的函数放在第二位,在IIFE执行之后当参数传递进去

1
2
3
4
5
6
7
(function IIFE (def){
def(window)
}(function def(global) {
var a =3
console.log(3)//3
console.log(global.a)//a
}))

3.4 块作用域

块作用域是对最小授权原则进行扩展的工具

3.4.1 with

用with从对象中创建出的作用域仅在with声明中有效

3.4.2 try/catch

catch会创建一个块级作用域,其中声明的变量仅在catch内部有效

3.4.3 let

let 可以将变量绑定到所在的任意作用域中({内部})
推荐显式的使用块级作用域,

1
2
3
4
5
if (foo) {
{
let bar
}
}

  1. 垃圾收集
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function process (data) {}
    var someReallyBigData = {}
    process(someReallyBigData)
    var btn = document.getElementById('my_button')
    btn.addEventListener('click', function click(evt) {
    //形成了闭包, 保留了整个外层作用域
    console.log('button clicked')
    }, false)

通过块级作用域,将变量进行本地绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function process (data) {}
{
let someReallyBigData = {}
process(someReallyBigData)
}
var btn = document.getElementById('my_button')
btn.addEventListener('click', function click(evt) {
//形成了闭包, 保留了整个外层作用域
console.log('button clicked')
}, false)

  1. let循环
    将i重新绑定到了循环的每一个迭代中
    1
    2
    3
    4
    5
    6
    7
    //代码说明
    {
    let j
    for (j=0; j<10; j++) {
    let i = j
    }
    }

let声明属于一个新的作用域而不是当前函数作用域也不是全局作用域

3.4.4

const固定常量不可修改

4 提升

4.2 编译器与提升

包含变量和函数在内的所有声明都会在任何代码被执行前首先被处理
定义声明在编译阶段进行,只有声明本身会被提升

4.3 函数优先

函数与变量声明都会提升,函数优先

5 作用域闭包

5.2 闭包实质

定义:函数可以记住并访问所在的词法作用域时,就产生了闭包。

1
2
3
4
5
6
7
8
9
function foo() {
var a = 2
function bar() {
console.log(a)
}
return bar
}
var baz = foo()
baz()

在自己定义的词法作用域之外执行,foo内存无法回收,导致foo内部作用域依然存在。
bar()依然持有对该作用域的引用,这个引用叫闭包

5.3 闭包深入

1
2
3
4
5
function wait (message) {
setTimeout(function timer(){
console.log(message)
}, 1000)
}
1
2
3
4
5
6
function setupBot (name, selector) {
$(selector).click(function activator(){
console.log('Activating:' + name)
})
}
setupBot('Closure Bot1', '#bot_1')

只要使用回调函数,实际上就是在使用闭包。

5.4 循环和闭包

1
2
3
4
5
6
for (var i=1; i<=5; i++) {
setTimeout(function timer(){
console.log(i)
}, 0)
}
//66666

定时器的回调函数在循环结束时才执行。

1
2
3
4
5
6
7
for (var i=1; i<=5; i++) {
(function(j) {
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
}

迭代内使用IIFE为每个迭代都生成一个新的作用域
延迟函数的回调在新的作用域封闭在每个迭代内部

块级作用域

1
2
3
4
5
for (let i=1; i<=5; i++) {
setTimeout(function timer(){
console.log(i)
}, 0)
}

let为每一次迭代生成块级作用域

5.5 模块

函数通过return,将内部函数暴露在外部,内部函数依旧保持函数的作用域。形成模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function CoolModule() {
var something = 'coll'
var another = [1, 2, 3]
function doSomething() {
console.log(something)
}
function doAnother() {
console.log(anoyher.join('!'))
}
return {
doSomething: doSomething,
doAnother: doAnother
}
}
var foo = CoolModule()
foo.donSomething()//coll
foo.doAnother()//1!2!3
//每次调用都会创建新的模块实例

模块模式的两个条件

  1. 必须有外部的封闭函数,至少被调用一次(创建模块实例)
  2. 封闭函数必须返回一个内部函数,内部函数才能在私有作用域中形成闭包

一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块

单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var foo = ( function CoolModule() {
var something = 'coll'
var another = [1, 2, 3]
function doSomething() {
console.log(something)
}
function doAnother() {
console.log(anoyher.join('!'))
}
return {
doSomething: doSomething,
doAnother: doAnother
}
})()
foo.donSomething()//coll
foo.doAnother()//1!2!3
//转换为IIFE,只生成了一个实例

命名将要作为公共API返回的对象

1
2
3
4
5
var foo = (function CollModule(id){
function change(){
publicAPI.identify = identify2
}
})

5.5.1 模块机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
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
}
//闭包,保持内部函数hello引用
})
MyModules.define('foo', ['bar'], function (bar) {
var hungry = 'hippo'
function awesome() {
console.log(bar.hello(hungry).toUpperCase())
}
return {
awesome: awesome
}
//闭包,保持内部函数awesome引用
})
var bar = MyModules.get('bar')
var foo = MyModules.get('foo')
console.log(bar.hello('hippo'))
foo.awesome()//

5.5.2 未来模块机制

ES6的模块语法支持,将单个文件当做独立模块处理

基于函数的模块不稳定(不能被编译器识别/动态)
es6模块可以再编译器检查导入模块的API和成员是否真实存在(可以进行静态检查)

5.6 小结

当函数可以记住并访问所在的词法作用域,既是函数是在当前词法作用域之外执行,这时产生了闭包

模块的两个主要特征:

  1. 为创建内部作用域而调用包装函数
  2. 包装函数的返回值至少包括一个队内部函数的引用,这样才能创建涵盖整个包装函数内部作用域的闭包

动态作用域

词法作用域特征:定义在代码书写阶段
动态作用域:作用域链是基于调用栈的,而不是作用域嵌套
js并不具有动态作用域,但是this机制某种程度上像动态作用域

1
2
3
4
5
6
7
8
9
10
11
function foo() {
console.log(a)//如果是动态作用域,理论上输出3
//实际上Js没有动态作用域,所以结果为2
}
function bar() {
var a = 3
foo()
}
var a = 2
bar()

this词法

箭头函数将当前的词法作用域覆盖this本来的值

缺点:

  1. 容易混淆this绑定规则和词法作用域规则
  2. 匿名而非具名的