Better

Ethan的博客,欢迎访问交流

JavaScript闭包、共享传递与call、bind和new的实现

考虑到内容太多问题,因此准备在学习深入理解过程中,分多次总结,本文续JavaScript深入理解(一)

闭包

  • 来源闭包
  • MDN定义

    闭包是指那些能够访问自由变量的函数。
    自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。
    闭包 = 函数 + 函数能够访问的自由变量

  • 理论上闭包

    从技术的角度讲,所有的JavaScript函数都是闭包。《JavaScript权威指南》

  • 实践上闭包,ECMAScript定义

    1. 从理论角度:所有的函数。
      • 因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
    2. 从实践角度:以下函数才算是闭包:
      • 即使创建它的上下文已经销毁,它仍然存在(比如内部函数从父函数中返回)
      • 在代码中引用了自由变量
  • 问题的关键在于创建他的上下文在父函数执行完后会被销毁,那么内部函数为什么还是访问到父函数的变量呢?
    • 我们知道内部函数f的上下文维护了一个作用域链,就是因为这个作用域链,f 函数依然可以读取到父函数 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。
  • 博主的例子很简单,但分析的很到位,观察下面的例子,看作用域链的的区别,也就理解了闭包。
    var data = [];
    for (var i = 0; i < 3; i++) {
    data[i] = function () {
      console.log(i);
    };
    }
    data[0]();
    data[1]();
    data[2]();
    
    data[0]作用域链为:
    data[0]Context = {
      Scope: [AO, globalContext.VO]
    }
    
    闭包例子:
    var data = [];
    for (var i = 0; i < 3; i++) {
    data[i] = (function (i) {
          return function(){
              console.log(i);
          }
    })(i);
    }
    data[0]();
    data[1]();
    data[2]();
    
    此时data[0]作用域链为:
    data[0]Context = {
      Scope: [AO, 匿名函数Context.AO globalContext.VO]
    }
    
    来一波分析,全局上下文的VO在代码执行中会有修改,当执行到data[0]函数时,VO已经是如下:
    globalContext = {
      VO: {
          data: [...],
          i: 3
      }
    }
    
    那么这就是为什么不用闭包的方式一得到的都是3,因为自身上下文中AO不存在i值,只能向上寻找找到全局上下文VO中的i值为3。使用闭包的方式二得到的1,2,3,因为多了一个匿名函数的AO,保存着此时此刻,对应的AO值中i分别为1,2,3。比如data[0]函数的父函数(表现为匿名函数)的AO如下:
    匿名函数Context = {
      AO: {
          arguments: {
              0: 0,
              length: 1
          },
          i: 0
      }
    }
    
    这样一来,data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是0。

按值传递

这一段看的很有感悟,推翻了之前按值传递和引用传递的理解方式,没想到还有共享传递这种方式。

  • 来源按值传递
  • JavaScript定义
    • ECMAScript中所有函数的参数都是按值传递的。
    • 按值传递:把函数外部的值复制给函数内部的参数。
  • 引用传递
    • 当值是一个复杂的数据结构的时候,拷贝就会产生性能上的问题。
    • 传递对象的引用,函数内部对参数的任何改变都会影响该对象的值,因为两者引用的是同一个对象。
  • 共享传递
    • 按引用传递是传递对象的引用,而按共享传递是传递对象的引用的副本,理解这个副本也就理解了共享传递
    • 与引用传递的区别:在共享传递中对函数形参的赋值操作,不会影响实参的值
    • 虽然引用是副本,引用的对象是相同的。它们共享相同的对象,所以修改形参对象的属性值,也会影响到实参的属性值。
    • 求值策略被用于Python、Java、Ruby、JS等多种语言。
  • 那么为什么说JS是按值传递呢?
    • 参数如果是基本类型是按值传递,如果是引用类型按共享传递。但是因为拷贝副本也是一种值的拷贝,所以在高程中也直接认为是按值传递了。

call&apply的实现

  • 来源call和apply
  • 功能:call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
  • 模拟实现关键
    • 改变this:将方法变成传入对象的属性,这样方法的this也就被改变了,同时避免污染对象,执行完后调用delete删除方法。
    • 传递参数:使用arguments解决参数不定长,使用eval解决模拟函数传参执行。
  • 最终call代码, call 的性能要高于 apply
    Function.prototype.call2 = function (context) {
      var context = context || window;
      context.fn = this;
      var args = [];
      for(var i = 1, len = arguments.length; i < len; i++) {
          args.push('arguments[' + i + ']');
      }
      var result = eval('context.fn(' + args +')');
      delete context.fn
      return result;
    }
    
  • apply和call类似,这里欣赏下代码
    Function.prototype.apply = function (context, arr) {
      var context = Object(context) || window;
      context.fn = this;
      var result;
      if (!arr) {
          result = context.fn();
      }
      else {
          var args = [];
          for (var i = 0, len = arr.length; i < len; i++) {
              args.push('arr[' + i + ']');
          }
          result = eval('context.fn(' + args + ')')
      }
      delete context.fn
      return result;
    }
    

bind的实现

  • 来源bind
  • 功能:bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )
  • 特点:
    • 返回一个函数
    • 可以传入参数
    • bind时可以传参,执行bind函数时也可以传参
    • 当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。
  • 代码
    Function.prototype.bind2 = function (context) {
     //如果不是方法,报错
      if (typeof this !== "function") {
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
      }
      //保留方法引用,返回函数中调用
      var self = this;
      //实现可以传参,截取第二个以后的所有参数
      var args = Array.prototype.slice.call(arguments, 1);
      var fNOP = function () {};
      var fBound = function () {
          //截取所有参数
          var bindArgs = Array.prototype.slice.call(arguments);
          //构造函数时,bind的this失效
          self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
      }
      fNOP.prototype = this.prototype;
      fBound.prototype = new fNOP();
      return fBound;//返回一个函数
    }
    
    使用fNOP中转的目的,一开始真心没看懂,在博主评论去请教了一下,博主耐心的回复了我,如果不进行中转,直接使用fBound.prototype = this.prototype实现继承的话,我们直接修改返回函数的prototype 的时候,也会直接修改绑定函数的 prototype。例子如下:
    function bar() {}
    var bindFoo = bar.bind2(null);
    bindFoo.prototype.value = 1;
    console.log(bar.prototype.value) // 1
    

new的实现

  • 来源new
  • 定义:new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一
  • 特点
    • 访问构造函数中的属性:通过apply实现
    • 访问prototype 中的属性:实例的__proto__ 属性会指向构造函数的 prototype,也正是因为建立起这样的关系,实例可以访问原型上的属性。
    • 如果构造函数有返回值,如果是一个对象,我们就返回这个对象,如果没有,我们该返回什么就返回什么。
  • 步骤
    • 首先新建一个对象
    • 然后将对象的原型指向 Constructor.prototype
    • 然后 Constructor.apply(obj)
    • 返回这个对象
  • 代码
    function objectFactory() {
      var obj = new Object(),
      Constructor = [].shift.call(arguments);
      obj.__proto__ = Constructor.prototype;
      var ret = Constructor.apply(obj, arguments);
      return typeof ret === 'object' ? ret : obj;
    }
    

闭包总结

闭包应该是一个很困惑新人的地方,真是每次看都会有新感觉,的确是一个很迷人的东西,今天看到一篇翻译medium文章,感觉这个角度也是很不错的。

执行上下文(execution context) 称为当前执行代码所处的 环境或者作用域。

当我们开始执行程序时,首先处于全局上下文中。在全局上下文中声明的变量,称为全局变量。当程序调用函数时,会发生什么?发生下面这几步:

  1. JavaScript 创建一个新的执行上下文 —— 局部执行上下文。
  2. 这个局部执行上下文有属于它的变量集,这些变量是这个执行上下文的局部变量。
  3. 这个新的执行上下文被压入执行栈中。将执行栈当成是用来跟踪程序执行位置的一种机制。

函数什么时候执行完?当遇到 return 语句或者结束括号 } 时。函数结束时,发生下面情况:

  1. 局部执行上下文从执行栈弹出。
  2. 函数把返回值返回到调用上下文。调用上下文是指调用该函数的的执行上下文,它可以是全局执行上下文也可以是另外一个局部执行上下文。这里的返回值怎么处理取决于调用执行上下文。
  3. 局部执行上下文被销毁。这点很重要 —— 被销毁。所有在局部执行上下文中声明的变量都被清除。这些变量不再可用。这也是为什么称它们为局部变量。

词法作用域:在局部执行上下文和全局执行上下文各有一些变量。JavaScript 的一个难点是如何寻找变量。如果在局部执行上下文没找到某个变量,那么到它的调用上下文中去找。如果在它的调用上下文也没找到,重复上面的查找步骤,直到在全局执行上下文中找(如果也没找到,那么就是 undefined )。

闭包工作原理:只要你声明一个新的函数并赋值给一个变量,你就保存了这个函数定义,也就形成了闭包。闭包包含函数创建时的作用域里的所有变量。这类似于一个背包。函数定义带着一个背包,包里保存了所有在函数定义创建时作用域里的变量。因此换个角度而言:闭包是函数创建时作用域内所有变量的集合。

在这里有个疑问,所有函数中仅仅使用了创建时作用域的一部分变量呢,他背着所有变量的话,岂不是很累,我通过了一个不知道是否合适的例子,如下:

16520821736185.png 16571227668978.png

在上图中,我隐约看到了只会背着它需要的数据,但函数创建时,并不会执行的,它是如何知道需要哪些变量呢?还是说这个[[Scopes]]的表意这么理解是错误的,待我深入研究哇。

强相关性

在开始也说过,从技术的角度讲,所有的JavaScript函数都是闭包。因此全局作用域下创建的函数也生成闭包。但是既然函数是在全局作用域下创建的,他们可以访问全局作用域下的所有变量。所以这和闭包的概念就不那么相关。当函数的返回值是一个函数时,闭包的概念就变得更加相关了。返回的函数可以访问不在全局作用域里的变量,但它们只存在于闭包里。

更多



留言