1.函数的声明
谈闭包前,我们先看看在javascript中的函数。
jacascript中有两种定义函数的方式,一种是函数声明,一种是函数表达式。
关于函数式声明:函数式声明有一个重要的特征就是函数声明提升!意思是在执行代码之前会先读取函数声明,这就意味着可以把函数声明放在调用它的语句后面。代码如下。
sayHello() // Hello word! 正确执行不会报错
function sayHello () {
// 函数体
console.log('Hello word!')
}
关于函数表达式:函数表达式不存在函数声明提升!这种形式看起来好像是常规的变量赋值语句,即创建一个函数并将它赋值给变量,这种情况下创建的函数叫做匿名函数,因为function关键字后面没有标识符。
代码如下
sayHello() // error,sayHello is not defined
let sayHello = function () {
// 函数体
console.log('Hello word!')
}
2.闭包
闭包的概念:闭包是指有权访问另一个函数作用域中变量的函数
创建闭包的常见方式就是在一个函数内部创建另一个函数,以下代码作为示例。
function createComparisonFunnction(prototypeName){
return function (obj1, obj2){
// 下面两行中访问了外部函数中的变量prototypeName
let val1 = obj1[prototypeName]
let val2 = obj2[prototypeName]
if(val1 < val2){
return -1
}else if(val1 > val2){
return 1
}else{
return 0
}
}
}
在上面的例子中,匿名函数被返回,并且可以在其他地方被调用,但是它仍然可以访问变量prototypeName。之所以还可以访问这个变量,是因为这个匿名函数的作用域链包含createComparisonFunnction的作用域。要搞清楚这里面的细节,必须从理解函数被调用的时候都会发生什么入手。
tips:函数执行过程的作用域链
我们以以下代码和图作为示例
function compare (val1, val2) {
if (val1 < val2) {
return -1
} else if (val1 > val2) {
return 1
} else {
return 0
}
}
var result = compare(5, 10)
图则表示compare函数执行的时候的作用域链,不做过多的说明。
其实作用域链的本质是一个指向变量对象的指针列表,它只是引用但不包含实际的变量对象。无论从什么时候在函数中访问一个变量,都会从作用域链中搜索具有相应名字的变量。
一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量)。但是闭包的情况又有所不同。
我们通过以下代码和图来理解匿名函数执行时的作用域链。(由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存,过渡使用闭包可能会导致内存使用过多)
// 构建函数
let compareNames = createComparisonFunnction('name')
let arg1 = {name: 'jack'}
let arg2 = {name: 'mike'}
// 调用函数
let result = compareNames(arg1, arg2)
// 解除对匿名函数的引用,以便释放内存
compareNames = null
3.闭包与变量
作用域链这种配置机制引出一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。别忘了闭包保存的时整个变量对象,而不是某个特殊的值。下面的例子可以很清晰地说明这个问题
function createFunc (num) {
let result = []
// 这里定义i使用的是var而不是let
for (var i = 0; i < num; i++) {
result[i] = function () {
console.log(i)
}
}
return result
}
let func = createFunc(10)
这个函数返回一个函数数组,每个函数的返回值都是10.因为每个函数的作用域链中都保存着createFunc()函数的活动对象(不是具体的值,而是对象的引用)。
当然我们可以使用ES6的let定义变量i可以解决这个问题。但是我们现在尝试通过创建另一个匿名函数强制让闭包的行为符合预期
function createFunc (num) {
let result = []
for (var i = 0; i < num; i++) {
// 使用立即执行函数
result[i] = function (num) {
return function () {
console.log(num)
}
}(i)
}
return result
}
let func = createFunc(10)
func[0]() // 0
4.闭包与this对象
在闭包中使用this对象可能会导致一些问题,我们知道,this对象是在运行时基于函数的执行环境绑定的:在全局环境中,this对象等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此this对象通常指向window,当然在通过call和apply改变函数执行环境的情况下另当别论。下面是一个例子
var name = 'the window'
var object = {
name: 'the object',
getName: function () {
return function () {
console.log(this.name)
}
}
}
object.getName()() // the window
// 因为匿名函数是在全局环境下执行的,所以this对象指向window
不过把外部作用域中的this对象保存在一个闭包能够访问的变量里,就可以让闭包访问到该对象,我们可以将上述代码稍作修改
var name = 'the window'
var object = {
name: 'the object',
getName: function () {
let _this = this
return function () {
console.log(_this.name)
}
}
}
object.getName()() // the object
tips:在几种特殊情况下,this的值可能会发生改变。我列举以下的情况
var name = 'the window'
var object = {
name: 'the object',
getName: function () {
console.log(this)
return this.name
}
}
console.log(object.getName()) // the object
console.log((object.getName)()) // the object
console.log((object.getName = object.getName)()) // the window,非严格模式下
主要在第三句,先执行了一条赋值语句,然后再调用赋值后的结果。因为这个函数表达式的值是函数本身,所以this的值不能得到维持,结果就返回了the window。
这里插一个题外话:上面的代码在node环境下会打印出undefined,而浏览器环境下打印的是the window,这是因为node环境下和浏览器环境下的this的指向不同,感兴趣的话可以自行百度啊。
分享到这里就结束了,希望能帮助到大家。