关注前端小讴,阅读更多原创技术文章
10.14 闭包
闭包是指引用了另一个函数作用域中变量的函数,通常在嵌套函数中实现(如果一个函数访问了它的外部变量,那么它就是一个闭包)
- 闭包中函数的作用域链中,有对外部函数变量的引用
- 为了在全局作用域可以访问到闭包函数,通常在外部函数内返回内部闭包函数
- 因此外部函数被闭包引用的活动对象,并不能在外部函数执行后被销毁,仍保留在内存中
- 若要释放内存,需解除闭包函数对外部函数活动对象的引用
function arraySort(key, sort) {
return function (a, b) {
// 内部匿名函数,引用外部函数变量key和sort,形成闭包
let va = a[key]
let vb = b[key]
if (sort === 'asc' || sort === undefined || sort === '') {
// 正序:va > vb
if (va > vb) return 1
else if (va < vb) return -1
else return 0
} else if (sort === 'desc') {
// 倒序:va < vb
if (va < vb) return 1
else if (va > vb) return -1
else return 0
}
}
}
let compareNames = arraySort('name') // 执行函数并返回匿名函数,匿名函数的作用域链仍对arraySort的活动对象key和sort有引用,因此不会被销毁
let result = compareNames({ name: 'Nicholas' }, { name: 'Matt' }) // 执行匿名函数
compareNames = null // 解除匿名函数对arraySort活动对象的引用,释放内存
10.14.1 this 对象
在闭包内使用
this
会让代码更复杂,若内部函数没有使用箭头函数,this
绑定给执行函数的上下文:- 全局函数中调用,非严格模式下
this
指向window
,严格模式等于undefined
- 作为某个对象的方法调用,
this
指向该对象 - 匿名函数不会绑定到某个对象,其
this
指向调用该匿名函数的对象
- 全局函数中调用,非严格模式下
global.identity = 'The Window' // vscode是node运行环境,无法识别全局对象window,测试时将window改为global
let object = {
identity: 'My Object',
getIdentityFunc() {
return function () {
return this.identity
}
},
}
console.log(object.getIdentityFunc()) // ƒ () { return this.identity },返回匿名函数
console.log(object.getIdentityFunc()()) // 'The Window',立即调用匿名函数返回this.identity,this指向全局对象
- 函数被调用时自动创建变量
this
和arguments
,内部函数不能直接访问外部函数的这两个变量,若想访问需将其引用先保存到闭包能访问的另一个变量中
let object2 = {
identity: 'My Object',
getIdentityFunc() {
let that = this // 外部函数的变量this保存在that中
return function () {
return that.identity // 闭包(匿名函数)中引用that,that指向getIdentityFunc()上下文的this(而非闭包内的this)
}
},
}
console.log(object2.getIdentityFunc()()) // 'My Object',立即调用匿名函数返回that.identity,that指向闭包外部函数getIdentityFunc的this
- 一些特殊情况下的
this
值
let object3 = {
identity: 'My Object',
getIdentity() {
return this.identity
},
}
console.log(object3.getIdentity()) // 'My Object'
console.log(object3.getIdentity) // [Function: getIdentity],函数getIdentity()
console.log((object3.getIdentity = object3.getIdentity)) // [Function: getIdentity],函数getIdentity()赋值给object3.getIdentity
console.log((object3.getIdentity = object3.getIdentity)()) // 'The Window',赋值后在全局立即调用匿名函数,this指向全局对象
console.log((object3.funcA = object3.getIdentity)()) // 'The Window',函数getIdentity()赋值给对象其他属性,结果相同
object3.funcB = object3.getIdentity
console.log(object3.funcB()) // 'My Object',赋值后在object3调用,this指向object3
10.14.2 内存泄漏
- 由于使用了不同的垃圾回收机制,闭包在 IE9 之前的 IE 浏览器会导致问题:一旦 HTML 元素保存在某个闭包的作用域中,其不会被销毁
function assignHandler() {
let element = document.getElementById('someElement')
element.onclick = () => {
console.log(element.id) // 引用外部函数的活动对象element,匿名函数一直存在因此element不会被销毁
}
}
function assignHandler2() {
let element = document.getElementById('someElement')
let id = element.id // 保存element.id的变量id
element.onclick = () => {
console.log(id) // 不直接引用element,改为引用改为引用保存着element.id的变量id
}
element = null // 解除对element对象的引用,释放闭包内存
}
10.15 立即调用的函数表达式
- 立即调用的匿名函数又称立即调用的函数表达式(IIFE),其类似于函数声明,被包含在括号中
;(function () {
// 块级作用域
})()
- ES5 尚未支持块级作用域,可以使用 IIFE 模拟
;(function () {
for (var i = 0; i < 3; i++) {
console.log(i) // 0、1、2
}
})()
console.log(i) // ReferenceError: i is not defined,i在函数体作用域(模拟块级作用域)内
- ES6 支持块级作用域,无须 IIFE 即可实现同样的功能
{
let i = 0
for (i = 0; i < 3; i++) {
console.log(i) // 0、1、2
}
}
console.log(i) // ReferenceError: i is not defined
for (let i = 0; i < 3; i++) {
console.log(i) // 0、1、2
}
console.log(i) // ReferenceError: i is not defined
- 执行单击处理程序时,迭代变量的值是循环结束时的最终值,可以用 IIFE 或块级作用域锁定每次单击要显示的值
let divs = document.querySelectorAll('div')
for (var i = 0; i < divs.length; ++i) {
divs[i].addEventListener(
'click',
// 错误的写法:直接打印(单击处理程序迭代变量的值是循环结束时的最终值)
// function(){
// console.log(i);
// }
// 正确的写法:立即执行的函数表达式,锁定每次要显示的值
(function (_i) {
return function () {
console.log(_i)
}
})(i) // 参数传入每次要显示的值
)
}
for (let i = 0; i < divs.length; ++i) {
// 用let关键字,在循环内部为每个循环创建独立的变量
divs[i].addEventListener('click', function () {
console.log(i)
})
}
- 同理,执行超时逻辑时,迭代变量的值是导致循环退出的值,同样可用 IIFE 或块级作用域锁定每次要迭代的值
for (var i = 0; i < 5; i++) {
// 超时逻辑在退出循环后执行,迭代变量保存的是导致循环退出的值5
setTimeout(() => {
console.log(i) // 5、5、5、5、5
}, 0)
}
for (var i = 0; i < 5; i++) {
// 用立即调用的函数表达式,传入每次循环的当前索引,锁定每次超时逻辑应该显示的索引值
;(function (_i) {
setTimeout(() => {
console.log(_i) // 0、1、2、3、4
}, 0)
})(i)
}
for (let i = 0; i < 5; i++) {
// 使用let声明:为每个迭代循环声明新的迭代变量
setTimeout(() => {
console.log(i) // 0、1、2、3、4
}, 0)
}
10.16 私有变量
- 任何定义在函数或块中的变量,都可以认为是私有的(函数或块的外部无法访问其中的变量)
- 私有变量包括函数参数、局部变量和函数内部定义的其他函数
function add(num1, num2) {
// 3个私有变量:参数num1、参数num2、局部变量sum
let sum = num1 + num2
return sum
}
- 特权方法是能够访问函数的私有变量(及私有函数)的公共方法,可在构造函数中实现
function MyObject() {
let privateVariable = 10
function privateFunction() {
console.log('privateFunction')
return false
}
// 特权方法(闭包):访问私有变量privateVariable和私有方法privateFunction()
this.publicMethod = function () {
console.log('privateVariable', privateVariable++)
return privateFunction()
}
}
let obj = new MyObject()
obj.publicMethod()
/*
privateVariable 10
privateFunction
*/
- 同构造函数的缺点,在构造函数中实现私有变量的问题是:每个实例都重新创建方法(私有方法&特权方法),机制相同的 Function 对象被多次实例化
function Person(name) {
/* 私有变量name无法被直接访问到,只能通过getName()和setName()特权方法读写 */
this.getName = function () {
return name
}
this.setName = function (_name) {
name = _name
}
}
let person = new Person('Nicholas') // 每创建一个实例都创建一遍方法(私有方法&特权方法)
console.log(person.getName()) // 'Nicholas'
person.setName('Greg')
console.log(person.getName()) // 'Greg'
10.16.1 静态私有变量
使用匿名函数表达式创建私有作用域,实现特权方法:
定义私有变量和私有方法
- 私有变量作为静态私有变量,被共享,但不存在于每个实例中
定义构造函数
- 使用函数表达式定义构造函数(函数声明会创建内部函数)
- 不使用关键字定义构造函数,使其创建在全局作用域中
定义公有方法(特权方法)
- 定义在构造函数的原型上
;(function () {
/* 匿名函数表达式,创建私有作用域 */
// 私有变量和私有方法,被隐藏
let privateVariable = 10
function privateFunction() {
return false
}
// 构造函数:使用函数表达式 & 不使用关键字(创建在全局作用域)
MyObject = function () {}
// 公有方法/特权方法(闭包):定义在构造函数的原型上
MyObject.prototype.publicMethod = function () {
console.log('privateVariable', privateVariable++)
return privateFunction()
}
})()
- 该方式下,私有变量和私有方法由实例共享,特权方法定义在原型上,也由实例共享
- 创建实例不会重新创建方法,但调用特权方法并修改静态私有变量会影响所有实例
;(function () {
// 私有变量name,被隐藏
let name = ''
// 构造函数,创建在全局作用域中
Person = function (_name) {
name = _name
}
// 特权方法,定义在构造函数原型上
Person.prototype.getName = function () {
return name
}
Person.prototype.setName = function (_name) {
name = _name
}
})()
let person1 = new Person('Nicholas')
console.log(person1.getName()) // 'Nicholas'
person1.setName('Matt')
console.log(person1.getName()) // 'Matt'
let person2 = new Person('Michael')
console.log(person2.getName()) // 'Michael',调用特权方法并修改静态私有变量
console.log(person1.getName()) // 'Michael',影响所有实例
10.16.2 模块模式
在单例对象基础上加以扩展,通过作用域链关联私有变量和特权方法:
- 将单例对象的对象字面量扩展为立即调用的函数表达式
- 在匿名函数内部,定义私有变量和私有方法
- 在匿名函数内部,返回只包含可以公开访问属性和方法的对象字面量
let singleton = (function () {
/* 立即调用的函数表达式,创建私有作用域 */
// 私有变量和私有方法,被隐藏
let privateVariable = 10
function privateFunction() {
return false
}
// 返回只包含可以公开访问属性和方法的对象字面量
return {
publicProperty: true,
publicMethod() {
console.log(++privateVariable)
return privateFunction
},
}
})()
console.log(singleton) // { publicProperty: true, publicMethod: [Function: publicMethod] }
singleton.publicMethod() // 11
- 本质上,该模式用对象字面量定义了单例对象的公共接口
function BaseComponent() {} // BaseComponent组件
let application = (function () {
let components = new Array() // 创建私有数组components
components.push(new BaseComponent()) // 初始化,将BaseComponent组件的新实例添加到数组中
/* 公共接口 */
return {
// getComponentCount()特权方法:返回注册组件数量
getComponentCount() {
return components.length
},
// registerComponent()特权方法:注册组件
registerComponent(component) {
if (typeof component === 'object') {
components.push(component)
}
},
// getRegistedComponents()特权方法:查看已注册的组件
getRegistedComponents() {
return components
},
}
})()
console.log(application.getComponentCount()) // 1
console.log(application.getRegistedComponents()) // [ BaseComponent {} ],已注册组件BaseComponent
function APPComponent() {} // APPComponent组件
application.registerComponent(new APPComponent()) // 注册组件APPComponent
console.log(application.getComponentCount()) // 2
console.log(application.getRegistedComponents()) // [ BaseComponent {}, APPComponent {} ],已注册组件BaseComponent和APPComponent
10.16.3 模块增强模式
利用模块模式,在返回对象前进行增强,适合单例对象为某个特定类型的实例,但必须给它添加额外属性或方法的场景:
- 在匿名函数内部,定义私有变量和私有方法
- 在匿名函数内部,创建某(特定)类型的实例
- 给实例对象添加共有属性和方法(增强)
- 返回实例对象
function CustomType() {} // 特定类型
let singleton2 = (function () {
// 私有变量和私有方法,被隐藏
let privateVariable = 10
function privateFunction() {
return false
}
// 创建特定类型的实例
let object = new CustomType()
// 添加公有属性和方法
object.publicProperty = true
object.publicMethod = function () {
console.log(++privateVariable)
return privateFunction
}
// 返回实例
return object
})()
console.log(singleton2) // CustomType { publicProperty: true, publicMethod: [Function: publicMethod] }
singleton2.publicMethod() // 11
- 以模块模式的单例对象公共接口为例,若
application
必须是BaseComponent
组件的实例,可以使用模块增强模式来创建:
let application2 = (function () {
let components = new Array() // 创建私有数组components
components.push(new BaseComponent()) // 初始化,将BaseComponent组件的新实例添加到数组中
let app = new BaseComponent() // 创建局部变量保存实例
/* 公共接口 */
// getComponentCount()特权方法:返回注册组件数量
app.getComponentCount = function () {
return components.length
}
// registerComponent()特权方法:注册组件
app.registerComponent = function (component) {
if (typeof component === 'object') {
components.push(component)
}
}
// getRegistedComponents()特权方法:查看已注册的组件
app.getRegistedComponents = function () {
return components
}
return app // 返回实例
})()
console.log(application2) // BaseComponent { getComponentCount: [Function (anonymous)], registerComponent: [Function (anonymous)], getRegistedComponents: [Function (anonymous)] }
console.log(application2.getComponentCount()) // 1
console.log(application2.getRegistedComponents()) // [ BaseComponent {} ],已注册组件BaseComponent
application2.registerComponent(new APPComponent()) // 注册组件APPComponent
console.log(application2.getComponentCount()) // 2
console.log(application2.getRegistedComponents()) // [ BaseComponent {}, APPComponent {} ],已注册组件BaseComponent和APPComponent
私有变量 & 私有方法 | 特权方法 | 缺点 | |
---|---|---|---|
构造函数 | 实例中,独立 | 实例中 | 每个实例重新创建方法(私有方法&特权方法) |
私有作用域 | 私有作用域中,静态,共享 | 构造函数原型上 | 调用特权方法修改私有变量,影响其他实例 |
模块模式 | 私有作用域中,独立 | 单例对象上 | |
模块增强模式 | 私有作用域中,独立 | 实例对象上 |
总结 & 问点
- 什么是闭包?其作用是什么?
- 在没有使用箭头函数的情况下,this 在全局和局部方法调用时,分别指向哪里?若是匿名函数中的 this 呢?
- 函数嵌套时,内部函数如何访问外部函数的 this 和 arguments?
- 什么是立即调用的函数表达式?请用代码用其模拟块级作用域
- 请用代码实现功能:获取所有的 div 元素,点击不同的 div 显示其相应的索引值。要求分别用 IIFE 和块级作用域实现
- 请用代码实现功能:1 秒后实现 0~4 的数字迭代。要求分别用 IIFE 和块级作用域实现
- 什么是私有变量?其可能包括哪些内容?
- 什么是特权方法?请写一段代码,在构造函数中实现特权方法,并说说这种方式有什么问题
- 请写一段代码,通过私有变量实现特权方法,说说并证明这种方式有什么局限
- 请写一段代码,通过模块模式定义单例对象的公共接口,实现 Web 组件注册
- 模块增强模式适合什么场景?请用代码实现其模式下的 Web 组件注册