- 发布于
《你不知道的JS》-作用域
- Authors
- Name
- 田中原
你不知道的 JS-作用域
目录
作用域
编译原理
分词/词法分析(Tokenizing/Lexing)
将字符串分解成代码块(词法单元)
分词和词法分析的差异在于词法单元的识别是通过
有状态
还是无状态的
解析/语法分析(Parsing)
将词法单元流(数组)转换为由元素逐级嵌套组成的代表程序语法结构的树(抽象语法树 AST)
代码生成
将 AST 转换为可执行代码
作用域
JS 运行
- 引擎:负责 JS 编译与执行
- 编译器:负责语法分析以及代码生成
- 作用域:收集并维护所有声明的变量(标识符),根据规则确保执行代码对变量的访问权限
声明解析
var a = 2
var a
编译器查询作用域是否存在变量a
,有则忽略继续编译,无则要求作用域在当前作用域集合中声明一个为a
的变量编译器生成运行代码给引擎,处理
a = 2
赋值操作。引擎询问作用域,当前作用域是否存在a
,存在就使用,否则继续查找。最终没有查找到将会报错。
LHS/RHS
变量出现在赋值操作左侧进行 LHS,非左侧进行 RHS
- LHS:只查找
- BHS:查找并取到值()
function foo(a) {
console.log(a)
//RHS,获得a的值传入console.log方法
//RHS,获得console对象,并得到log方法
}
foo(2)
//函数调用RHS,查找foo并获得foo的值
//LHS查询,将2赋值给参数a
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
作用域嵌套
作用域嵌套,在当前作用域无法找到变量时,引擎会在外层作用域中继续查找
异常
RHS 查询找不到会抛出异常 非严格 LHS 找不到会创建一个 严格模式 LHS 找不到也会抛出异常 RHS 进行不合理操作会抛出 TypeError
词法作用域
两种工作模型
- 词法作用域
- 动态作用域
词法阶段
定义在词法阶段的作用域。(写代码是将变量和块作用域写在哪里决定的)
查找:
遮蔽效应:内部标识符”遮蔽”外部标识符
欺骗词法
欺骗词法作用域会导致性能下降
eval
eval 中包含一个或多个声明会对作用域进行修改 严格模式下,eval 有自己的词法作用域
setTimeout(),setInterval()第一个参数是字符串的时候,字符串可以被解释为动态生成的函数代码
new Function()接受代码字符串,转换为动态生成的函数
避免使用以上方式
with
严格模式下被禁止
性能
在编译阶段进行的性能优化,有些依赖于能够根据代码词法进行静态分析,预先确定所有变量和函数的定义位置,才能执行时快速找到标识符。
函数作用域和块作作用域
函数中的作用域
Js 基于函数的作用域 含义:属于这个函数的全部变量都可以在整个函数范围内使用以及复用
隐藏内部实现
最小授权,最小暴露原则:最小限度的暴露必要内容
规避冲突:避免同名标识符之间的冲突
全局命名空间
通过全局作用域生命对象,将功能通过对象暴露给外界
模块管理
函数作用域
让函数名不污染所在作用域,并能够自动运行 函数表达式写法 如果 function 是声明的第一个词,就是函数声明,否则是函数表达式 函数声明和函数表达式的区别:名称标识符绑定在何处 函数声明会绑定在所在作用域中 函数表达式会绑定在函数表达式自身的函数中
匿名和具名
匿名函数的缺点:
- 调试困难
- 引用自身需要
arguments.callee
- 可读性差
行内函数表达式可以指定函数名,所以给函数表达式命名是最佳实践
立即执行函数表达式 IIFE
IIFE 的两种写法
- (function())()
- (function()())
IIFE 的用途
- 函数调用传参
;(function (global) {})(window)(function (undefined) {})()
// 可以确保undefined是undefined
2.倒置代码运行顺序 将需要运行的函数放在第二位,在 IIFE 执行之后当参数传递进去
;(function IIFE(def) {
def(window)
})(function def(global) {
var a = 3
console.log(3) //3
console.log(global.a) //a
})
块作用域
块作用域是对最小授权原则进行扩展的工具
with
用 with 从对象中创建出的作用域仅在 with 声明中有效
try/catch
catch 会创建一个块级作用域,其中声明的变量仅在 catch 内部有效
let
let 可以将变量绑定到所在的任意作用域中(内部) 推荐显式的使用块级作用域,
if (foo) {
{
let bar
}
}
通过块级作用域,将变量进行本地绑定
function process(data) {}
{
let someReallyBigData = {}
process(someReallyBigData)
}
var btn = document.getElementById('my_button')
btn.addEventListener(
'click',
function click(evt) {
//形成了闭包, 保留了整个外层作用域
console.log('button clicked')
},
false
)
let 循环将 i 重新绑定到了循环的每一个迭代中
//代码说明
{
let j
for (j = 0; j < 10; j++) {
let i = j
}
}
let 声明属于一个新的作用域而不是当前函数作用域也不是全局作用域
const 固定常量不可修改
提升
编译器与提升
包含变量和函数在内的所有声明都会在任何代码被执行前首先被处理 定义声明在编译阶段进行,只有声明本身会被提升
函数优先
函数与变量声明都会提升,函数优先
作用域闭包
闭包实质
定义:函数可以记住并访问所在的词法作用域时,就产生了闭包。
function foo() {
var a = 2
function bar() {
console.log(a)
}
return bar
}
var baz = foo()
baz()
在自己定义的词法作用域之外执行,foo 内存无法回收,导致 foo 内部作用域依然存在。 bar()依然持有对该作用域的引用,这个引用叫闭包
闭包深入
function wait(message) {
setTimeout(function timer() {
console.log(message)
}, 1000)
}
function setupBot(name, selector) {
$(selector).click(function activator() {
console.log('Activating:' + name)
})
}
setupBot('Closure Bot1', '#bot_1')
只要使用回调函数,实际上就是在使用闭包。
循环和闭包
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 0)
}
//66666
定时器的回调函数在循环结束时才执行。
for (var i = 1; i <= 5; i++) {
;(function (j) {
setTimeout(function timer() {
console.log(j)
}, 0)
})(i)
}
迭代内使用 IIFE 为每个迭代都生成一个新的作用域,延迟函数的回调在新的作用域封闭在每个迭代内部
块级作用域
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, 0)
}
let 为每一次迭代生成块级作用域
模块
函数通过 return,将内部函数暴露在外部,内部函数依旧保持函数的作用域。形成模块
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
//每次调用都会创建新的模块实例
模块模式的两个条件
- 必须有外部的封闭函数,至少被调用一次(创建模块实例)
- 封闭函数必须返回一个内部函数,内部函数才能在私有作用域中形成闭包
一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块
单例模式
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,只生成了一个实例
模块机制
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() //
未来模块机制
ES6 的模块语法支持,将单个文件当做独立模块处理
基于函数的模块不稳定(不能被编译器识别/动态) es6 模块可以再编译器检查导入模块的 API 和成员是否真实存在(可以进行静态检查)
小结
当函数可以记住并访问所在的词法作用域,既是函数是在当前词法作用域之外执行,这时产生了闭包
模块的两个主要特征:
- 为创建内部作用域而调用包装函数
- 包装函数的返回值至少包括一个队内部函数的引用,这样才能创建涵盖整个包装函数内部作用域的闭包
动态作用域
词法作用域特征:定义在代码书写阶段 动态作用域:作用域链是基于调用栈的,而不是作用域嵌套 js 并不具有动态作用域,但是 this 机制某种程度上像动态作用域
function foo() {
console.log(a) //如果是动态作用域,理论上输出3
//实际上Js没有动态作用域,所以结果为2
}
function bar() {
var a = 3
foo()
}
var a = 2
bar()
this 词法作用域
箭头函数将当前的词法作用域覆盖 this 本来的值
缺点:
容易混淆 this 绑定规则和词法作用域规则
匿名而非具名的
var a
编译器查询作用域是否存在变量a
,有则忽略继续编译,无则要求作用域在当前作用域集合中声明一个为a
的变量编译器生成运行代码给引擎,处理
a = 2
赋值操作。引擎询问作用域,当前作用域是否存在a
,存在就使用,否则继续查找。最终没有查找到将会报错。