作用域
在学习作用域之前,先了解两个重要的概念:编译器、引擎
编译器:负责词法分析及代码生成等编译过程
引擎:负责整个 JavaScript
程序的编译和执行
什么是作用域
通俗的来讲就是变量起作用的范围。比较规范的解释(引用《你不知道的 JavaScript 》上卷),负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行代码对这些标识符的访问权限。
在ES6
之前,JavaScript只有全局作用域和函数作用域,与其他类型语言不同的是它没有块级作用域。
if(true){
var a = 1;//全局作用域
}
console.log(a); // 1
function foo(){
var b = 1;//函数作用域
console.log(a); //1
}
console.log(b); // ReferenceError
在上面的代码中,a
属于全局作用域,if
后的花括号并没有形成块级作用域,而 b
属于 foo
函数的作用域,在JavaScript
中函数外部作用域访问不到函数内部作用域,所以在全局作用域中访问foo
函数作用域变量b
会报错。
在es6
之后,JavaScript
拥有了块级作用域
if (true) {
let a = 1
}
console.log(a) // ReferenceError
在if
、for
、while
、try...catch
等在大括号中使用let
、const
声明的变量会形成块级作用域,如果在外部访问会报错。
作用域如何工作
变量提升
刚开始接触 JavaScript
的同学可能会对变量先声明后使用的现象十分不解,要理解它我们得了解JavaScript
编译的两个原则:①编译时声明 ②运行时赋值
var a = 2;
//相当于↓
var a; //编译时
a = 2; //运行时
上面这段代码 var a = 2
只做一件事,对a
进行赋值 ,不过浏览器引擎不这么看, 它会被分为 var a
和 a = 2
两步进行,一个在编译器编译时声明变量,另一个在引擎运行时赋值。
编译器首先将上面这段程序分解为词法单元,然后将词法单元解析成一个树结构(AST
抽象语法树)。在开始代码生成时,编译器遇到var a
,编译器询问作用域是否已经声明了这个变量;如果是,编译器忽略该声明,否则在当前作用域集合声明一个新的变量,命名为a
。
引擎执行a = 2
首先询问作用域,在当前的作用域集合中是否存在一个叫做a
的变量。如果是,引擎就会使用这个变量,否则引擎会继续延着作用域链查找该变量。如果引擎最终找到了a
变量,就会将 2 赋值给它,否则引擎会抛出一个异常Uncaught ReferenceError: a is not defined
函数提升
a() // aaa => 函数a被提升,所以在声明前可以调用函数
var a
function a () {
console.log('aaa')
}
console.log(a) // ƒ a() {} 函数声明优先级比变量声明高
var
声明的变量会提升,function
声明的函数也会被提升,并且函数声明优先级比变量声明优先级高,所以上面这段代码打印 a
是个函数,因为var a
声明的变量被function
声明的函数覆盖了。
词法作用域
词法作用域就是定义在词法阶段的作用域,也就是说作用域是在书写代码时函数声明的位置来决定,与执行过程无关,JavaScript
采用的是词法作用域。
相对词法作用域另外一种叫做动态作用域,作用域是在执行阶段确定的,比如Bash
脚本、Perl
语言等。
看下面这段代码示例:
var a = 1
function foo () {
console.log(a)
}
function bar () {
var a = 'local'
foo ()
}
bar() // 词法作用域是:1 ;动态作用域是:‘local’
我们使用词法作用域和动态作用域分析一下上面这段代码执行过程,bar
函数内部调用 foo
函数
如果是词法作用域,调用 foo
查找变量a
会从foo
函数代码定义的位置向外一层也就是全局作用域访问,此时var a = 1
,结果是 1;
如果是动态作用域,调用foo
查找变量a
会从当前调用函数位置开始向往搜索,发现外部声明var a = 'local'
,所以 a
的值是local
;
而在JS
引擎中上面这段代码运行结果是 1,所以JavaScript
采用的是词法作用域
不过,this
在 JavaScript
中比较特殊,JavaScript
程序在执行的时候才会对this
进行赋值,在未执行时不能知道this
的作用域,所以比较准确的说在JavaScript
中this
采用的是动态作用域。
修改词法作用域: eval 和 with
eval 欺骗词法作用域
eval
函数接收一个或多个声明的代码,会修改其所处的词法作用域。
var a = 2
function foo (str, b) {
eval(str) // 欺骗
console.log(a, b)
}
foo('var a = 3', 1) // 3, 1
执行 eval
函数,传入的字符串会解析成脚本执行,声明一个变量 a
修改了 foo
函数的词法作用域,遮蔽了外部(全局)作用域中的同名变量访问,欺骗了 foo
词法作用域。另外,使用 eval
函数还容易受到xss
攻击。
with 欺骗词法作用域
with
将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,如果对象中没有该标识号,会在全局创建一个新的词法作用域
with
的用法
var obj = {
a: 1,
b: 2,
c: 3
}
// 对象属性赋值,多次使用obj
obj.a = 2
obj.b = 3
obj.c = 4
// 使用 with 写法简洁
with(obj) {
a = 3;
b = 4;
c = 5;
}
with
的缺陷
function foo(obj) {
with(obj) {
a = 2
}
}
var obj1 = {
a: 3
}
var obj2 = {
b: 3
}
foo(obj1)
console.log(obj1.a) // 2
foo(obj2)
console.log(obj2.a) // undefined
console.log(a) // 2 —— a被泄露到了全局作用域上
with
会修改引用中属性的值,如果引用中没有该属性,在非严格模式下会在全局作用域中创建一个全新的词法作用域,欺骗了全局词法作用域
除此之外,使用 eval
和 with
还会带来性能问题,因为JS
引擎无法在编译时对它们作用域进行查询优化,这样会导致代码运行效率变慢,所以建议不要使用它们。
作用域链
作用域链形成是由词法作用域和编译时词法环境对外部环境引用的结果,关于词法环境外部环境的引用可以参考这篇文章【深入了解JavaScript执行过程】
)
现在主要说说作用域链的构成过程,开始执行脚本时创建全局作用域,在全局环境调用 foo
函数 时,编译foo
函数并创建foo
函数作用域,foo
函数中声明 bar
函数,在调用 bar
函数会创建 bar
函数作用域。JavaScript
中,内部函数可以访问外部函数的变量,这样, bar
函数作用域 =》 foo
函数作用域 =》全局作用域 构成了一条作用域链。
var a = 'global'
function foo () {
var b = 'foo scoped'
function bar () {
var c = 'bar scoped'
console.log(a, b, c)
}
bar()
}
}
foo() // 'global' 'foo scoped' 'bar scoped'
闭包
谈起闭包,它可是JavaScript两个核心技术之一(异步和闭包),在面试以及实际应用当中,我们都离不开它们,甚至可以说它们是衡量js
工程师实力的一个重要指标。下面我们就罗列闭包的几个常见问题,从回答问题的角度来理解和定义闭包。
问题如下:
- 什么是闭包
- 闭包的原理是什么
- 闭包是如何使用的
- 闭包的应用场景有哪些
如果你能回答上面这些问题,说明你对闭包非常熟悉了;如果脑子里比较模糊回答不上来也不用担心,继续往下读,相信你会找到答案的。
什么是闭包
网上有很多种对闭包解释的说法:
1、闭包是由函数以及创建该函数的词法环境组合而成
2、闭包是能够读取其他函数内部变量的函数
读起来比较抽象和拗口,用代码来理解闭包。
function foo() {
var a = 2
function bar () {
console.log(a)
}
return bar
}
var baz = foo()
baz() // 2 —— 这就是闭包的效果
函数是一等公民,可以当成数值来使用,它既可以作为函数参数,也可以作为函数返回值。调用foo
函数返回bar
,理论上来说foo
函数执行完之后会被销毁,不过bar
函数引用着foo
的a
变量,所以执行完foo
,函数体会被销毁,但是a
被引用着不能被回收仍然保存在内存当中,所以在外部调用bar
函数可以访到foo
内部函数的a
变量。这时我们给foo
起了另外一个名字叫闭包函数。
我们知道根据作用域链函数内部可以访问函数外部的变量,反过来是不行的,但是闭包可以做到,这就是闭包的神奇之处
总结一下,闭包本质上是一个函数,它返回另一个函数,可以使外部函数可以访问其他函数内部的变量。
闭包原理
细心的朋友可能知道答案了,闭包的原理就是词法作用域和作用域链形成的结果。
如何使用闭包
为了能让我们的程序更健壮,我们往往需要将实现细节隐藏起来,只对外提供暴露接口,这也是面向对象三大特性之一封装性
私有变量
function foo () {
var num = 0
function bar () {
++num
return num
}
return bar
}
var add1 = foo ()
add1() // 1
add1() // 2
add1() // 3
var add2 = foo ()
add2() // 1
add2() // 2
add2() // 3
每次执行foo
都得到相同的值,不会相互污染
function Person() {
var age = 20
var sex = 'man'
getAge () {
return age
}
setAge(value) {
age = value
}
getSex () {
return sex
}
setSex(value) {
sex = value
}
return {
getAge,
setAge,
getSex,
setSex
}
}
var zhangsan = Person()
zhangsan.getAge() // 20
zhangsan.getSex() // 男
隐藏实现细节,对外暴露接口。模拟实现了面向对象的思想,代码也显得健壮、易理解、可扩展可维护。
闭包的应用场景
定时器、事件监听器、Ajax
请求、跨窗口通信、Web Workers
或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是使用闭包
闭包使用注意事项
1、闭包会使得函数中的变量都被保存在内存中,内存消耗很大,处理不当,容易造成内存泄漏
2、如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响。
总结
写的内容有点多,梳理一下
1、首先讲了什么是作用域,作用域类型分为全局作用域、函数作用域、函数作用域
2、其次作用域工作时,使用var
和functioin
声明会出现变量提升和函数提升;JavaScript
是词法作用域,eval
和 with
会欺骗词法作用域
3、最后讲了作用域链的原理和闭包使用介绍
引用链接
推荐阅读