# 1. 作用域、this、闭包、执行上下文
# 1.1. 变量提升例题
如果像下面例子那样,采用function
命令和var
赋值语句声明同一个函数,由于存在函数提升,最后会采用var
赋值语句的定义。
var f = function () {
console.log('1');
}
function f() {
console.log('2');
}
f() // 1
上面例子中,表面上后面声明的函数f
,应该覆盖前面的var
赋值语句,但是由于存在函数提升,实际上正好反过来。
# 1.2. 函数本身的作用域
函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。
var a = 1;
var x = function () {
console.log(a);
};
function f() {
var a = 2;
x();
}
f() // 1
上面代码中,函数x
是在函数f
的外部声明的,所以它的作用域绑定外层,内部变量a
不会到函数f
体内取值,所以输出1
,而不是2
。
总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。
# 1.3. 闭包
function f1() {
var n = 999;
function f2() {
console.log(n);
}
return f2;
}
var result = f1();
result(); // 999
上面代码中,函数f1
的返回值就是函数f2
,由于f2
可以读取f1
的内部变量,所以就可以在外部获得f1
的内部变量了。
闭包就是函数f2
,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。
闭包最大的特点,就是它可以“记住”诞生的环境,比如f2
记住了它诞生的环境f1
,所以从f2
可以得到f1
的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
闭包的最大用处有两个,一个是可以读取其他函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。
请看下面的例子,闭包使得内部变量记住上一次调用时的运算结果。
function createIncrementor(start) {
return function () {
return start++;
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
上面代码中,start
是函数createIncrementor
的内部变量。通过闭包,start
的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包inc
使得函数createIncrementor
的内部环境,一直存在。所以,闭包可以看作是函数内部作用域的一个接口。
为什么会这样呢?原因就在于inc
始终在内存中,而inc
的存在依赖于createIncrementor
,因此也始终在内存中,不会在调用结束后,被垃圾回收机制回收。
闭包的另一个用处,是封装对象的私有属性和私有方法。
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}
return {
name: name,
getAge: getAge,
setAge: setAge
};
}
var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25
上面代码中,函数Person
的内部变量_age
,通过闭包getAge
和setAge
,变成了返回对象p1
的私有变量。
注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
# 1.4. 函数表达式会变量提升吗?
fn1();
var fn1=function(){} // 报错(fn1 is not a function),函数表达式
fn2();
function fn2(){} // undefined,函数声明
可见,函数声明才会出现变量提升,函数表达式不会。 变量提升的其他例子:
console.log(a);
var a=9; //undefined
fn6('zhang');
function fn6(name){
console.log(name)
} // zhang
fn('zhang');
function fn(name){
age =20;
console.log(name, age);
var age
} //zhang 20
可以看出,函数提升就可以执行,变量只是声明。 再看几个例子:
fn('zhang');
function fn(name){
console.log(name, age);
var age=10
} // zhang undefined
var fo = 1;
var foobar = function(){
console.log(fo);
var fo=2;
}
foobar();
// undefined
// 既不是1也不是2,因为函数中的 var fo 存在变量提升,会覆盖全局的 fo
# 1.5. 什么是执行上下文?
当代码运行时,会产生一个对应的执行环境,在这个环境中,所有变量会被事先提出来(变量提升),有的直接赋值,有的为默认值 undefined,代码从上往下开始执行,就叫做执行上下文。
在 JavaScript 中,运行环境有三种,分别是:
- 全局环境:代码首先进入的环境,也就是一段
<script>
- 函数环境:函数被调用时执行的环境
- eval函数(不常用)
全局范围内什么会出现变量提升?
- 变量定义
- 函数声明
函数内什么会出现变量提升?
- 变量定义
- 函数声明
- this
- arguments
# 1.6. 执行上下文特点
- 单线程,在主进程上运行
- 同步执行,从上往下按顺序执行
- 全局上下文只有一个,浏览器关闭时会被弹出栈
- 函数的执行上下文没有数目限制
- 函数每被调用一次,都会产生一个新的执行上下文环境
# 1.7. 什么是执行上下文栈?
执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。 其实这是一个压栈出栈的过程——执行上下文栈。
函数中调用其他函数:
# 1.8. this使用场景
- 作为构造函数
- 作为对象属性
- 作为普通函数
- call apply bind
普通函数 ,this就是window
# 1.9. this的特点
this要在执行时才能确认值,定义时无法确认:
var a = {
name : 'A',
fn: function() {
console.log(this.name)
}
}
a.fn() // 此时 this 等于 a
a.fn.call({ name: 'b' }) // 此时 this 等于{ name: 'b' }
var fn1 = a.fn()
fn1() // 此时 this 等于 window
# 1.10. js 的作用域有哪些?
js没有块级作用域,只有函数和局部作用域:
// 无块级作用域
if (true) {
var name = 'ZhangSan'
}
console.log(name) // 'ZhangSan'
// 函数和全局作用域
var a = 'LiBai'
function fn() {
var a = 'DuFu'
console.log(a)
}
console.log('global', a) // 'LiBai'
fn() // 'DuFu'
# 1.11. 什么是自由变量和作用域链?
- 当前作用域(函数或全局)没有定义的变量,就是自由变量。
- 当前函数没有定义a,就一层一层去父级作用域寻找,形成链式结构,成为作用域链。
var a = 100
function fn() {
var b = 200
console.log(a) // 当前作用域没有定义的变量,即自由变量
console.log(b)
}
fn()
再看一个例子:
var a = 100
function fn1() {
var b = 200
function fn2() {
var c = 300
console.log(a) // 自由变量
console.log(b) // 自由变量
console.log(c)
}
fn2()
}
fn1()
# 1.12. 什么是js的闭包?有什么作用,用闭包写个单例模式
MDN对闭包的定义是:闭包是指那些能够访问自由变量的函数,自由变量是指在函数中使用的,但既不是函数参数又不是函数的局部变量的变量。
由此可以看出,闭包=函数+函数能够访问的自由变量,所以从技术的角度讲,所有JS函数都是闭包,但是这是理论上的闭包。还有一个实践角度上的闭包。
从实践角度上来说,只有满足1、即使创建它的上下文已经销毁,它仍然存在,2、在代码中引入了自由变量,才称为闭包。
闭包的应用:
- 模仿块级作用域
- 保存外部函数的变量
- 封装私有变量
闭包的实现 (opens new window) 单例模式,参见 前端面试手写代码 (opens new window)
# 1.13. 为什么闭包函数能够访问其他函数的作用域?
顺着作用域链一层一层往上找
# 1.14. 写一个自执行函数
( function(i){ console.log(i)} ) (2) // 2
# 1.15. 判断打印顺序
for(var i = 0; i < 5; i++){
setTimeout((function() {
return function() {
console.log(i++)
}
})(), 0)
}
// 5 6 7 8 9
# 1.15. 判断打印结果
var length = 10
function fn() {
console.log(this.length)
}
var obj = {
length: 5,
method: function(fn) {
fn()
arguments[0]()
}
}
obj.method(fn, 1)
// 10
// 2
← jQuery service_worker →