作用域
概念: JS中的作用域指变量能被访问的范围。作用域根据其能被访问的范围的不同,分成了全局作用域、函数作用域、块级作用域三种,其中块级作用域是ES6中新增的概念。
全局作用域
变量挂载在window(或global)对象下,可以在何处都能被访问到,这个变量就是全局变量,对应着全局作用域。
函数作用域
函数中定义的变量叫函数变量,这个时候只能在函数内部才能访问到它,所以它的作用域也就是函数的内部,称为函数作用域。
块级作用域
使用let关键词定义的变量只能在块级作用域中被访问,并且根据let定义的变量有暂时性死区的特点,在变量定义之前无法被使用。
简单的个人理解就是在{...}中使用了let或const定义一个变量,那么这个变量就只能在{...}中被访问,而对应的就形成了块级作用域。
console.log(a) //a is not defined
if (true) {
let a = '123' // 在if(){...}中声明的变量a只能在这里面被调用,其他地方都无法被使用
console.log(a) // 123
}
console.log(a) //a is not defined
// 在声明前使用b,用var会使变量提升,打印undefined,而使用let则会造成暂时性死区。
console.log(b) // undefined
if (true) {
var b = '456'
// 使用var声明变量b,能够在if外面被访问,这就是使用let后产生的块级作用域与var的差别。
console.log(b) // 456
}
console.log(b) // 456
使用立即调用函数可以模仿块级作用域:
(function () {
// 块级作用域
})();
作用域链
一般变量的访问会有一个顺序,看下列代码:
var a = 1
function test () {
// 如果只在这里声明var a = 2,并且函数fun中没有声明a,那么最终打印结果就是2
function fun () {
// 这里声明var a = 3的话那下面打印的a就是3了
console.log(a)
}
return fun
}
test()() // 1
函数fun对a的访问有一个顺序,先从函数fun的作用域中去寻找变量a,找不到之后再去上层test函数的作用域中寻找变量a,要是再找不到,那就去全局作用域中寻找,这样一层一层的访问,就形成了作用域链。
同时,内部环境可以通过作用域链访问所有外部环境,但外部环境不能访问内部环境的任何变量和函数,也就是说如果想在函数test中访问函数fun中声明的变量,是访问不到的。
可以使用
with (expression) { statement}来将某个对象添加到作用域链的前端,但是不推荐使用with,在 ECMAScript 5 严格模式中该标签已被禁止。推荐的替代方案是声明一个临时变量来承载你所需要的属性。
try/catch的catch块则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。
注意点: 在函数中声明变量时要加上var,不然就是声明一个全局变量。
function foo () {
// 不加var时这里就会变成全局变量,可以在函数外部被访问到。
a = 'hello'
}
foo()
console.log(a) // 'hello'
闭包
一般概念: 闭包指的是引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
(之前对这个概念总觉得是函数中return一个函数就是闭包,这其实是最一般的理解,闭包是不一定需要在函数中返回另一个函数的,只需要函数之间的变量进行相互引用就行了。)
在MDN中对于闭包的概念如下:
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
个人理解:
根据一般的闭包概念跟MDN中的概念来看,只要涉及到两个作用域之间的变量调用,就是闭包。类似下面的例子:
var a = 'hello'
function foo () {
console.log(a)
}
上述代码就是在函数foo的作用域里面访问了全局作用域中的变量a,这在我看来就是一个闭包。
其实个人更喜欢这个定义:函数和函数内部能访问其他作用域的变量的总和,就是一个闭包。
这也解释了为什么在MDN定义的概念最后有这么一句:每当创建一个函数,闭包就会在函数创建的同时被创建出来。
闭包的优点
- 方便调用其他作用域中的变量。
- 避免变量写在全局而造成污染。
- 让被调用的变量一直在内存中,可以做缓存。(也是缺点,导致内存消耗大)
闭包的缺点
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大。可以在退出函数之前,将不使用的局部变量全部删除来解决这个问题。
// 使用一个红宝书的例子:
function assignHandler () {
var element = document.getElementById('someElement')
/**
循环引用引起的内存泄漏,是因为IE 的bug,
循环引用无法自动判断,所以通过拷贝值,把内外引用脱钩, 这样就可回收。
IE9及其以后已修复。
*/
var id = element.id // 解除循环引用
element.onclick = function () {
console.log(id)
}
element = null // 解除对document.getElementById('someElement')对象的引用
}
这里要提醒自己,只是内存消耗大,而不是以前所认为的造成内存泄漏,内存泄漏的原因在于IE的bug,在使用完闭包之后无法回收闭包所引用的变量。是IE的bug而不是闭包本身的问题。并且内存泄漏是指访问不到的变量占据内存空间,但是闭包在执行的时候所访问的变量都是需要用到的。
注意点:
在闭包中使用 this 会产生一些问题。如果内部函数没有使用箭头函数,则this对象会在运行时绑定到执行函数的上下文。
var name = "Window"
var object = {
name: "Object",
getName: function () {
return function(){
return this.name
}
}
}
console.log(object.getName()())
整两个问题
问题一: 下面代码输出啥?
function fun (a, b) {
console.log(b)
return {
fun: function (c) {
return fun(c, a)
}
}
}
var d = fun(0)
d.fun(1)
d.fun(2)
d.fun(3)
var d1 = fun(0).fun(1).fun(2).fun(3)
var d2 = fun(0).fun(1
d2.fun(2)
d2.fun(3)
输出结果:
// fun有两个参数a、b,这里只传了0,因此b是undefined
var d = fun(0) // undefined
/**
fun被调用后返回一个对象d:
d = {
fun: function (c) {
return fun(c, a)
}
}
d.fun(1)后,执行fun(c, a),其中a为上次传入的0, c为当前传入的1,
就相当于执行了fun(1, 0),此时打印的b就是0了
*/
d.fun(1) // 0
// 这里d一直都是第一次执行d = fun(0)返回的对象,相当于执行fun(2, 0),因此打印的还是0
d.fun(2) // 0
// 与上同
d.fun(3) // 0
/**
这里是连续调用,打印的值会有所变化;
第一次执行的是fun(0, undefined),此时打印undefined、返回对象
{
fun: function (c) {
return fun(c, a)
}
}
第二次执行的时候,由于第一次的执行完成的a为0,此时又传入c为1,就相当于执行了fun(1, 0)
因此打印的是0;
第三次执行了fun(2, 1);打印1
第四次执行了fun(3, 2);打印2
都是因为返回函数中有对上级函数作用域中a的值的引用,所以a会一直存在,
然而在调用返回的对象中的fun的时候,又改变了a的值,因此打印的结果就会发生变化。
*/
var d1 = fun(0).fun(1).fun(2).fun(3) // undefined、0、1、2
// 上述两种情况的综合,不多说
var d2 = fun(0).fun(1) // undefined、0
d2.fun(2) // 1
d2.fun(3) // 1
问题二: 输出结果是什么?
for (var i = 0; i < 6; i++) {
setTimeout(function () {
console.log(i)
}, i * 1000)
}
由于for中的
var i = 0其实是创建了一个全局变量i,并且setTimeout是异步的,因此会先全部执行完for循环语句之后再执行setTimeout中的函数,所以结果是每隔1秒输出一个6;
扩展: 如何使其每隔一秒打印出0,1,2,3,4,5?
- 方案一:使用let定义i,使得for(...) { ... }形成块级作用域。
for (let i = 0; i < 6; i++) {
setTimeout(function () {
console.log(i)
}, i * 1000)
}
这段代码相当于执行了:
{
// 形成块级作用域
let i = 0
{
let ii = i
setTimeout(function () {
console.log(ii)
}, i * 1000 )
}
i++
{
let ii = i
setTimeout(function () {
console.log(ii)
}, i * 1000 )
}
i++
{
let ii = i
setTimeout(function () {
console.log(ii)
}, i * 1000 )
}
...
}
- 方案二:使用立即执行函数
for (var i = 0; i < 6; i++) {
(function (i) {
setTimeout(function () {
console.log(i)
}, i * 1000)
})(i)
}
用立即执行函数形成函数作用域,并且把i当成参数传入,这样得话每个函数作用域里面都有一个变量i,等到setTimeout中得函数执行时,访问到的就是当前立即执行函数作用域里面得i。
- 方案三:使用setTimeout的第三个参数
for (var i = 0; i < 6; i++) {
setTimeout(function (j) {
console.log(j)
}, i * 1000, i)
}