关注前端小讴,浏览更多原创技术文章

相干代码 →

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改为globallet 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指向全局对象
  • 函数被调用时主动创立变量thisarguments外部函数不能间接拜访内部函数的这两个变量,若想拜访需将其援用先保留到闭包能拜访的另一个变量中
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.getIdentityconsole.log((object3.getIdentity = object3.getIdentity)()) // 'The Window',赋值后在全局立刻调用匿名函数,this指向全局对象console.log((object3.funcA = object3.getIdentity)()) // 'The Window',函数getIdentity()赋值给对象其余属性,后果雷同object3.funcB = object3.getIdentityconsole.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 definedfor (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()) // 1console.log(application.getRegistedComponents()) // [ BaseComponent {} ],已注册组件BaseComponentfunction APPComponent() {} // APPComponent组件application.registerComponent(new APPComponent()) // 注册组件APPComponentconsole.log(application.getComponentCount()) // 2console.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()) // 1console.log(application2.getRegistedComponents()) // [ BaseComponent {} ],已注册组件BaseComponentapplication2.registerComponent(new APPComponent()) // 注册组件APPComponentconsole.log(application2.getComponentCount()) // 2console.log(application2.getRegistedComponents()) // [ BaseComponent {}, APPComponent {} ],已注册组件BaseComponent和APPComponent
公有变量 & 公有办法特权办法毛病
构造函数实例中,独立实例中每个实例从新创立办法(公有办法&特权办法)
公有作用域公有作用域中,动态,共享构造函数原型上调用特权办法批改公有变量,影响其余实例
模块模式公有作用域中,独立单例对象上
模块加强模式公有作用域中,独立实例对象上

总结 & 问点

  • 什么是闭包?其作用是什么?
  • 在没有应用箭头函数的状况下,this 在全局和部分办法调用时,别离指向哪里?若是匿名函数中的 this 呢?
  • 函数嵌套时,外部函数如何拜访内部函数的 this 和 arguments?
  • 什么是立刻调用的函数表达式?请用代码用其模仿块级作用域
  • 请用代码实现性能:获取所有的 div 元素,点击不同的 div 显示其相应的索引值。要求别离用 IIFE 和块级作用域实现
  • 请用代码实现性能:1 秒后实现 0~4 的数字迭代。要求别离用 IIFE 和块级作用域实现
  • 什么是公有变量?其可能包含哪些内容?
  • 什么是特权办法?请写一段代码,在构造函数中实现特权办法,并说说这种形式有什么问题
  • 请写一段代码,通过公有变量实现特权办法,说说并证实这种形式有什么局限
  • 请写一段代码,通过模块模式定义单例对象的公共接口,实现 Web 组件注册
  • 模块加强模式适宜什么场景?请用代码实现其模式下的 Web 组件注册