JavaScript之Function函数深刻总结

整理了JavaScript中函数Function的各类,感受函数就是一大对象啊,各类知识点都能牵扯进来,不仅仅是 Function 这个自己原生的引用类型的各类用法,还包含执行环境,做用域,闭包,上下文,私有变量等知识点的深刻理解。javascript

函数中的returnhtml

  1.  return 语句能够不带有任何返回值,在这种状况下( return; 或函数中不含 return 语句时),函数在中止执行后将返回 undefiend 值。这种用法通常在须要提早中止函数执行而又不须要返回值的状况下。
  2.  return false 能够取消元素的默认行为,但不能阻止事件传播。只能是return false,return别的值不行,这里的处理方式是DOM的事件处理机制作出的约定。DOM的事件传播有两个类型,一个是捕获(从父节点到子节点),一个是冒泡(从子节点到父节点),因此一个事件触发时能够有多个处理器去处理它,事实上,仅仅是在HTML事件属性 和 DOM0级事件处理方法中 才能经过返回 return false 的形式组织事件宿主的默认行为。详见:https://www.zhihu.com/question/19867421
  3.  return 返回的是其所在函数的返回值
    function n(){
      (function(){
         return 5;
      })();
    }
    n();// undefined
    //当即执行匿名函数中的return语句实际上是返回给它所在的匿名函数的。
    
    function n(){
      var num= (function(){
         return 5;
      })();
      console.log(num);
    }

 

Function类型
函数其实是对象,每一个函数实际上都是 Function 类型的实例。并且与其余引用类型同样具备属性和方法。函数名其实是一个指向内存堆中某个函数对象的指针。前端

定义函数的方式java

  1. 函数声明
    function sum(num1,num2){
      return num1+num2;
    }
  2. 函数表达式
    var sum=function(num1,num2){
     return num1+num2;
    };
    定义了一个变量 sum 并将其初始化为一个函数,注意到 function 关键字后面并无函数名,这是由于在使用函数表达式定义函数,不必使用函数名,经过变量 sum 便可引用函数。还要注意函数末尾有个分号,就像声明其余变量同样。
  3.  new 构造函数,虽然这种用法也是函数表达式,但该用法不推荐。由于这种语法会致使解析两次代码(第一次是解析常规的ECMAScript代码,第二次是解析传入构造函数中的字符串),影响性能。
    使用 Function 构造函数,构造函数能够接受任意数量的参数,但最后一个参数始终都被当作是函数体,前面的参数则枚举出了新函数的参数。
    var sum=new Function('num1','num2','return num1+num2;');
    sum;// 
    function anonymous(num1,num2
    /**/) {
    return num1+num2;
    }

    当使用不带圆括号的函数名是访问函数指针,而非调用函数。git

 

理解参数
ECMAScript中全部参数传递的都是值(即便是引用也是传递的地址值,不是引用传递参数(可参考 JavaScript传递参数是按值传递仍是按引用传递))。ECMAScript函数不介意传递进来多少个参数,也不在意传进来的参数是什么数据类型。之因此这样,是由于ECMAScript中的参数在内部是用一个数组表示的。函数接收到的始终都是这个数组,而不关心数组中包含哪些参数。在函数体内,能够经过 arguments 对象来访问这个数组。从而获取传递给函数的每一个参数。github

function func(){
 console.log(Object.prototype.toString.call(arguments));
}

func();// [object Arguments]
  1. 关于 arguments 的行为,它的值永远与对应命名参数的值保持同步。由于 arguments 对象中的值会自动反映到对应的命名参数。因此修改 arguments[1] ,也就修改了 num2 。不过这并非说读取这两个值会访问相同的内存空间,它们的内存空间是独立的,但他们值会同步(WHY??),要是JavaScript能直接访问内存就行了验证一下。
  2. 但若是只传入了一个参数,那么 arguments[1] 设置的值不会反映到命名参数中,这是由于 arguments 对象的长度是由传入参数个数决定的,不是由定义函数时的命名参数个数决定的,没有传递值的命名参数将自动被赋予 undefiend 值,这就跟定义了变量但没初始化同样。
    function doAdd(num1,num2){
      console.log(arguments.length);
      console.log(num2)
      arguments[1]=10;
      console.log(num2);
    }
    doAdd(5,0);//2 0 10
    
    doAdd(5);//1 undefiend undefined

 

没有重载web

ECMAScript函数不能像传统意义上那样实现重载,而在其余语言中(Java),能够为一个函数编写两个定义,只要这两个定义的签名(接收参数的类型和数量)不一样便可。
不能实现重载的缘由:算法

  1. ECMAScript函数没有签名,由于其参数是由包含零个或多个值的数组来表示的。没有函数签名,真正的重载是不可能作到的。在ECMAScript中定义两个名字相同的的函数,则该名字只属于后定义的函数。如何实现相似于Java中的重载呢,其实能够经过判断传入函数的参数类型和个数来作出不一样响应。
    function reload(){
       if(arguments.length==0){
           console.log('没传参');
       }else if(arguments.legth==1){
          console.log('传了一个参数');
      }
    }
  2. 深刻理解:将函数名想象为指针,也有助于理解为何ECMAScript中没有函数重载的概念。
    function add(){
      return 100;
    }
    function add(num){
     return num+200; 
    }
    
    //实际上和下面代码没什么区别
    function add(){
      return 100;
    }
    add=function(num){
     return num+200; 
    }

 

函数声明和函数表达式chrome

实际上解析器在向执行环境中加载数据时,对函数声明和函数表达式并不是一视同仁。
JavaScript运行机制浅探 中了解到对于解释型语言来讲,编译步骤为:编程

  1. 词法分析(将字符流转换为记号流,是一对一的硬性翻译获得的是一堆难理解的记号流)
  2. 语法分析(这里进行所谓的变量提高操做,其实我以为是把这些提高的变量保存在语法树中。要构造语法树,若发现没法构造就会报语法错误,并结束整个代码块的解析)
  3. 以后可能有语义检查,代码优化等。获得语法树后就开始解释执行了。解释性语言没有编译成二进制代码而是从语法树开始执行。

解析器会先读取函数声明,并使其在执行任何代码以前可用。至于函数表达式,则必须等到执行阶段才会被真正赋值。什么意思呢?虽然二者都进行了变量提高,待真正执行时构造活动对象从语法树种取声明添加到执行环境中,但一个是函数提高,一个是变量提高。

//函数声明
console.log(func);//function func(){}
function func(){

}

//函数表达式
console.log(func1);// undefined
var func1=function(){};
console.log(func1);// function(){}

 

 

做为值的函数

由于ECMAScript中的函数名自己就是变量,因此函数也能够做为值来使用。不只能够像传递参数同样把一个函数传递给另外一个函数,并且能够将一个函数做为另外一个函数的结果返回。

function callSomeFunction(someFunction,someArgument){
  return someFunction(someArgument);
}

function concated(str){
  return "Hi "+str;
}

callSomeFunction(concated,'xx');// 'Hi xx' 

从一个函数中返回另外一个函数的应用:假设有一个对象数组,想要根据某个对象属性对数组进行排序,但传给 sort() 方法的比较函数要接收两个参数,即要比较的。咱们须要一种方式来指明按照哪一个属性来排序。咱们能够定义一个函数它接收一个属性名,而后根据这个属性名来建立一个比较函数。默认状况下, sort 函数会调用每一个对象的 toString() 方法以肯定它们的次序。

function createCompare(property){
  return function(obj1,obj2){
    var value1=obj1[property],
        value2=obj2[property];   
    if(value1<value2) return -1;
    else if(value1>value2)  return 1;
    else return 0;
  }
}
var data=[{name:'aa',age:20},{name:'bb',age:12},{name:'cc',age:30}];
data.sort(createCompare("age"));// [{name:'bb',age:12},{name:'aa',age:20},{name:'bb',age:30}]

 

 

函数的内部属性

 arguments :类数组对象,包含传入函数中全部参数。是每一个函数自身的属性,之因此能够直接访问 arguments ,是由于命名空间??如下变化是为了增强JavaScript语言的安全性,这样第三方代码就不能在相同的环境下窥视其余代码了。

  •  callee 属性:是一个指针,指向拥有 arguments 对象的函数。严格模式访问会致使错误。
    //通常阶乘函数
    function factorial(num){
       if(num<=1){ return 1;}
       else {
         return num*factorial(num-1);
      }
    }

    定义阶乘函数用到递归算法,这样定义是没问题。
    缺点:这个函数的执行与函数名 factorial 牢牢耦合在一块儿。万一出现改变函数指向的这种状况就不太好了,

    factorial=function(){}
    factorial(3);// undefiend

    为了消除这种现象。

  • function factorial(num){   
       if(num<=1){     return 1;    }
       else{     
          return num*arguments.callee(num-1);    
      } 
    }

    这样不管引用函数使用的是什么名字均可以保证完成递归。

  •  caller 属性:不过在非严格模式下这个属性始终是 undefiend 。即便在严格模式下访问也会出错。增长这个属性是为了分清 arguments.caller 和函数对象上的 caller 属性。
    function a(){
    return Object.getOwnPropertyNames(arguments);
    }
    a();// ["length", "callee"]

 this :行为与Java/C#中的 this 大体相似。 this 引用的是函数据以执行环境对象(当在网页的全局做用域中调用函数时, this 对象引用的就是 window )。
 caller :不止是ECMAScript5中新增函数对象上的属性,仍是 arguments 上的属性。保存着调用当前函数的函数的引用。若是是在全局做用域中调用当前函数,它的值为 null 。
 Object.getOwnPropertyNames(Function);// ["length", "name", "arguments", "caller", "prototype"] 

function outer(){
  inner();
}
function inner(){
  console.log(inner.caller); //为了实现更松散的耦合,arguments.callee.caller
}

outer();// function outer(){ inner()}

严格模式下不能为函数的 caller 属性赋值,不然会致使出错。

 

函数的属性和方法

  • length:表示函数但愿接收的命名参数的个数(也就是定义的形参的个数)。
    function sayName(name){
      //
    }
    function sum(num1,num2){
      //
    }
    function sayHi(){
     // 
    }
    
    sayName.length;// 1
    sum.length;// 2
    sayHi.length;// 0
  • prototype:对于ECMAScript中的引用类型而言,prototype是保存它们全部实例方法的真正所在。诸如toStringvalueOf等方法实际上都保存在Object.prototype名下(原生构造函数好比Function,Array等 在本身原型上重写了toString)。在ECMAScript5中,prototype属性是不可枚举的,所以使用for-in没法发现。 Object.getOwnPropertyDescriptor(Function,'prototype');// Object {writable: false, enumerable: false, configurable: false}
  • 每一个函数上有两个可用的方法:applycall。这两个方法其实是在Function.prototype上, Object.getOwnPropertyNames(Function.prototype);// ["length", "name", "arguments", "caller", "apply", "bind", "call", "toString", "constructor"] 它是在JavaScript引擎内部实现的。由于是属于Function.prototype,因此每一个Function的实例均可以用(自定义的函数也是Function的实例)。都是在特定的做用域或自定义的上下文中调用执行函数,实际上等于设置函数体内 this 对象的值。
  1.  apply :参数一为在其中运行函数的做用域,参数二为参数数组(能够是数组,也能够是 arguments 对象)。
    function sum(num1,num2){
      return num1+num2;
    }
    
    function callSum1(num1,num2){
      return sum.apply(this,arguments);//sum.apply(this,[num1,num2])
    }
    
    callSum1(10,30);// 40

     严格模式下,未指定环境对象而调用函数, this 值不会转型为 window 。除非明确把函数添加到某个对象或者调用 apply 或 call ,不然 this 值将是 undefined 

  2.  call :参数一没有变化,变化的是其他参数都是直接传递给函数,参数必须都列出来。
    function callSum1(num1,num2){
      retrun sum.call(this,num1,num2);
    }
    
    callSum1(10,30);// 40

     call 和 apply 真正强大的地方是可以扩充函数赖以运行的做用域,改变函数的执行环境。

  3.  bind :ECMAScript5定义的方法,也是 Function.prototype 上的方法。用于控制函数的执行上下文,返回一个新函数,这个函数的 this 值会被绑定到传给 bind() 函数中的值。
    window.color="red";
    var o={color:'blue'};
    function sayColor(){
      console.log(this.color);
    }
    
    var newobj=sayColor.bind(o);
    newobj;// function sayColor(){
      console.log(this.color);
    }
    newobj==sayColor;// false
    newobj();// blue

     深刻理解:能够将函数绑定到指定环境的函数。接收一个函数和一个环境,返回在给定环境中调用给定函数的函数。

    function bind(func,context){
      return function(){
        func.apply(context,arguments);//这里建立了一个闭包,arguments使用的返回的函数的,而不是bind的
      }
    }

    当调用返回的函数时,它会在给定环境中执行被传入的函数并给出全部参数。

    function bind(func,context,args){
       return function(){
          func.call(context,args);
       };
    }
  4.  toString,toLocaleString :返回函数代码的字符串形式,返回格式因浏览器而异,有的返回源码,有的返回函数代码的内部表示,因为存在差别,用这个也实现不了什么功能。
  5.  valueOf :返回函数的自身引用。

 

变量,做用域,内存问题
JavaScript接近词法做用域,变量的做用域是在定义时决定而不是在执行时决定,也就是说词法做用域取决于源码。
JavaScript引擎在执行每一个函数实例时,都会为其建立一个执行环境,执行环境中包含一个AO变量对象,用来保存内部变量表,内嵌函数表,父级引用列表等语法分析结构(变量提高在语法分析阶段就已经获得了,并保存在语法树中,函数实例执行时会将这些信息复制到AO上)。

ECMA-262定义,JavaScript松散类型的本质决定了它只在特定时间用于保存特定值的一个名字而已,因为不存在定义某个变量必需要保存何种数据类型值得规则,变量的值及其数据类型可在脚本的生命周期内改变。

  • 基本类型和引用类型的值:ECMAScript变量可能包含两种不一样数据类型的值:基本类型值,引用类型值。
  1. 基本类型值:简单的数据段。
  2. 引用类型值:那些可能由多个值构成的对象。是保存在内存中的对象,JavaScript不容许直接访问内存中的位置,也就说不能直接操做对象的内存空间。在操做对象时其实是在操做对象的引用而不是实际的对象。为此,引用类型值是按引用访问的。(这种说法不严密,当复制保存着对象的某个变量时,操做的是对象的引用。但在为对象添加属性时,操做的是实际的对象)
    在将一个值赋给变量时,解析器必须肯定这个值是基本类型值仍是引用类型值。5种基本数据类型: Undefined,Null,Boolean,Number,String (不少语言中字符串以对象形式来表示所以被认为是引用类型,但ECMAScript放弃这一传统)。这5种基本类型是按值访问的,所以能够操做保存在变量中的实际的值。
  • 动态的属性
  • 复制变量的值:在从一个变量向另外一个变量复制基本类型值和引用类型值时,也存在不一样。
    若是从一个变量向另外一个变量复制基本类型的值,会在变量对象上建立一个新值,而后把该值复制到为新变量分配的位置上。
    当从一个变量向另外一个变量赋值引用类型值值时,一样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中,不一样的是,这个值的副本其实是个指针(能够理解为复制了地址值),而这个指针指向存储在堆中一个对象。复制操做结束后两个变量实际上将引用同一个对象。
  • 传递参数:ECMAScript中全部函数的参数都是按值传递的,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另外一个变量同样。基本类型值得传递如同基本类型变量的复制同样,引用类型值的传递如同引用类型变量的复制同样。不少人错误认为:在局部做用域中修改的对象会在全局做用域中反映出来这就说明是按引用传递的。为了证实对象是按值传递的,
    function setName(obj){
      obj.name="xx";
      obj=new Object();
      obj.name="bb";
    }
    
    var p=new Object();
    setName(p);
    p.name;// "xx"

    若是是按引用传递的,即传递的不是地址值而是堆内存中整个p对象,在 setName 中为其添加了一个新名字叫 obj ,又给其添加 name 属性后,将这个 obj 内容从新填充为新对象,那么以前的那个对象就不存在了更别说有 "xx" 的名字属性,可是 p.name 仍然访问到了。这说明即便在函数内部修改了参数值,但原始的引用仍然保持未变。实际上,当在函数内部重写 obj 时,这个变量引用的就是一个局部对象了,而这个局部对象会在函数执行完毕后被当即销毁。

  • 类型检测:检测一个变量是否是基本数据类型用 typeof 是最佳工具,但若是变量的值是除了函数的对象或 null  typeof [];// "object" typeof null;// "object" ,变量值为函数时 typeof function(){};// "function" (ECMA-262规定任何在内部实现 [[call]] 方法的对象都应该在应用 typeof 操做符返回 "function" )。但在检测引用类型值时,这个操做符用处不大,由于咱们并非想知道它是个对象,而是想知道它是某种类型对象。若是变量是给定引用类型的实例, instanceof 操做符会返回 true 。全部引用类型值都是 Object 的实例。若是使用 instanceof 操做符检测基本类型的值,则该操做符始终会返回 false ,由于基本类型不是对象。

 

执行环境及做用域

  • 执行环境(execution context):也称为做用域,定义了变量或函数有权访问的其余数据,决定了它们各自的行为。全局执行环境是最外围的一个执行环境,跟据ECMAScript实现所在的宿主环境不一样,表示执行环境的对象也不同,web浏览器中全局执行环境是 window 对象。某个执行环境中全部代码执行完毕后该环境被销毁,保存在其中的全部变量和函数定义也随之销毁(全局执行环境直到应用程序退出例如关闭网页或浏览器时才被销毁)。每一个函数都有本身的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中,在函数执行后,栈将其环境弹出,将控制权返回给以前的执行环境。ECMAScript程序中的执行流正是由这个机制控制着。函数的每次调用都会建立一个新的执行环境。执行环境分为建立和执行两个阶段,
  1. 建立:解析器初始化变量对象或者活动对象,它由定义在执行环境中的变量,函数声明,参数组成。在这个阶段,做用域链会被初始化,this的值也最终会被肯定。
  2. 执行:代码被解释执行
  • 变量对象(variable object):环境中定义的全部变量和函数都保存在这个对象中。虽然用代码没法访问它,但解析器在处理数据时会在后台使用它。若是这个环境是函数,则将活动对象(activation object)做变量对象
  • 做用域(scope)和上下文(context):函数的每次调用都有与之紧密相关的做用域和上下文。做用域是基于函数的,上下文是基于对象的。做用域涉及到被调函数中变量的访问,上下文始终是 this 关键字的值,它是拥有当前所执行代码的对象的引用。上下文一般取决于函数是如何被调用的。
  • 做用域链(scope chain):当代码在一个环境中执行时,会建立变量对象的一个做用域链。它是保证对执行环境有权访问的全部变量和函数的有序访问。做用域链的前端始终都是当前执行的代码所在环境的变量对象。活动对象在最开始时只包含一个变量即 arguments 对象(这个对象在全局环境中不存在),做用域链的下一个变量对象来自包含(外部)环境,再下一个变量对象则来自下一个包含环境,这样一直延续到全局执行环境。
    var color = "blue";
    function changeColor(){
      if(color=="blue"){
         color="red";
      }else{
         color="blue";
      }
    }
    changeColor();
    console.log(color);// red

    标识符解析是沿着做用域链一级一级地搜索标识符的过程,函数 changeColor 做用域链包含两个对象:它本身的变量对象(其中定义着 arguments 对象)和全局环境的变量对象。能够在函数内部访问到变量 color 就是由于能够在这个做用域链中找到它。内部环境能够经过做用域链访问全部外部环境,但外部环境不能访问内部环境的任何变量和函数。函数参数也被看成变量来对待,所以其访问规则与执行环境中的其余变量相同。

  • 延长做用域链:有些语句能够在做用域的前端临时添加一个变量对象,该变量对象会在代码执行后被移除。当执行流进入下列语句时,做用域链就会加长。
  1.  try-catch 语句的 catch 块:对 catch 语句来讲,会建立一个新的变量对象,其中包含的是被抛出的错误对象的声明。<=IE8版本中,在 catch 语句中捕获的错误对象会被添加到执行环境的变量对象而不是 catch 语句的变量对象,换句话说,即便是在 catch 块的外部也能够访问到错误对象。
  2.  with 语句:会将指定的对象添加到做用域链中。
    function buildUrl(){
       var qs="?debug=true";
       with(location){
          var url=href+qs;
       }
      return url;
    }
    buildUrl();// "http://i.cnblogs.com/EditPosts.aspx?postid=5280805?debug=true"

      with 语句接收的是一个 location 对象,所以其变量对象中就含有 location 对象的全部属性和方法,且这个变量对象被添加到了做用域链的最前端。当在 with 语句中引用变量 href (实际引用的是 location.href )能够在当前的执行环境中找到,当引用变量 qs 时,引用的则是在下一级执行环境中的变量。因为JavaScript中没有块级做用域,因此在函数内部能够访问 url 才能 return 成功,说明 url 并非添加到 location 所在的变量对象中。
    这两个语句都会在做用域的前端添加一个变量对象。

  • 没有块级做用域:在其余类C的语言中,由花括号封闭的代码块都有本身的做用域(若是用ECMAScript的话来说,就是他们本身的执行环境),于是支持根据条件来定义变量。若是是在C/C++/Java中, color 会在 if 语句执行完后被销毁,但在JavaScript中, if 语句中的变量声明会将变量添加到当前的执行环境中。
    if(true){
      var color="red";
    }
    
    console.log(color);// red
  1. 声明变量:使用 var 声明的变量会自动被添加到最接近的环境中。在函数内部,最接近的环境就是函数的局部环境;在 with 语句中,最接近的环境是函数环境。若是初始化变量时没有使用 var 声明,该变量会自动被添加到全局环境。
  2. 查询标识符:当在某个环境中为了读取或写入而引用一个标识符时,必须经过搜索来肯定该标识符表明什么。搜索过程从做用域链的前端开始,向上逐级查询与给定名字匹配的标识符。若是在局部环境中找到了该标识符,搜索过程中止,变量就绪。若是在局部环境中未找到该变量名,则继续沿做用域链向上搜索。搜索过程将一直追溯到全局环境。若是局部环境存在同名标识符,就不会使用位于父环境中的标识符。

 

函数表达式

if(condition){
  function sayHi(){
      console.log("Hi");
  }
}else{
  function sayHi(){
      console.log("Yo");
  }
}

以上代码会在 condition 为 true 时使用 sayHi() 的定义,不然就使用另外一个定义。实际上这在ECMAScript中属于无效语法,JavaScript引擎会尝试修正错误,将其转换为合理的状态。但问题是浏览器尝试修正的作法不同。大多数浏览器会返回第二个声明。此种方式很危险,不该该出现你的代码中。在chrome中:

if(true){
  function sayHi(){
      console.log("Hi");
  }
}else{
  function sayHi(){
      console.log("Yo");
  }
}//function sayHi(){ 没有函数声明的变量提高??
      console.log("Hi");
  }
if(false){
  function say(){
      console.log("Hi");
  }
}else{
  function say(){
      console.log("Yo");
  }
}//function say(){
      console.log("Yo");
  }
console.log(sa);//undefined 能输出undefiend说明函数声明并无提高而是进行的变量提高
if(false){
  function sa(){
      console.log("Hi");
  }
}else{
  function sa(){
      console.log("Yo");
  }
}//function sa(){
      console.log("Yo");
  }

修正:使用函数表达式,那就没什么问题了。

var sayHi;
if(condition){
  sayHi=function(){
     console.log("Hi");
  }
}else{
  sayHi=function(){
     console.log("Yo");
  }
}

 

递归

在严格模式下,不能经过脚本访问 arguments.callee 。不过可使用命名函数表达式来完成相同结果。

var factorial=(function f(num){
    if(num<=1){
        return 1;
    }else{
        return num*f(num-1);
   } 
});

注意是用命名函数表达式,单单把命名函数赋值给 factorial 也能够,可是并不能经过f的名字访问

 

闭包

闭包是指有权访问另外一个函数做用域中的变量的函数。建立闭包的常见方式,就是在一个函数内部建立另外一个函数。之因此可以访问外部做用域的变量,是由于内部函数的做用域链中包含外部做用域。当一个函数被调用的时候,

  1. 建立一个执行环境(execution context)及相应的做用域链
  2. 使用 arguments 和其余命名参数的值来初始化活动对象(activation object),但在做用域链中,外部函数的活动对象始终始终处于第二位...直至做为做用域链终点的全局执行环境。
    function compare(value1,value2){
       if(value1<value2){
          return -1;
       }else if(value1>value2){
          return 1;
       }else{
          return 0;
       }
    }
    
    var result=compare(5,10);

    当调用 compare() 时,会建立一个包含 arguments , value1 , value2 的活动对象,全局执行环境的变量对象(包含 result 和 compare )在 compare() 执行环境的做用域链中处于第二位。

    后台的每一个执行环境都有一个表示变量的对象(变量对象),全局环境的变量对象始终存在,而像 compare() 函数这样的局部环境的变量对象,则只在函数执行过程当中存在。在建立 compare() 函数时,会建立一个预先包含全局对象的做用域链,这个做用域链被保存在 compare 内部的 [[Scope]] 属性中。当调用 compare() 函数时,会为函数建立一个执行环境,而后经过复制函数的 [[Scope]] 属性中的对象构建起执行环境的做用域链。此后又有一个活动对象被建立并被推入执行环境做用域链的最前端。对于这个例子中, compare 函数的执行环境而言,其做用链包含两个变量对象:本地活动对象和全局变量对象。显然,做用域链的本质上是一个指向变量对象的指针列表,它只引用但不包含实际的变量对象。

不管何时在函数中访问一个变量,就会从做用域链中搜索具备相应名字的变量,通常来说当函数执行完后,局部活动对象会被销毁,内存中仅保留着全局做用域(全局执行环境的变量对象)。可是闭包的状况又有所不一样。在另外一个函数内部定义的函数会将包含函数(外部函数)的活动对象添加到它的做用域链里,当外部函数执行完后其活动对象不会被销毁,由于匿名函数的做用域链仍然在引用这个活动对象。换句话说只是外部函数它本身的做用域链被销毁,但活动对象还存在内存中。直到内部函数被销毁后(例如在外部解除了对闭包即内部函数的引用: func=null; ,解除至关因而闭包仅是执行完后),外部函数的活动对象才会被销毁。

因为闭包会携带包含它的函数的做用域,所以会比其余函数占用更多的内存。过多使用闭包可能会致使内存占用过多,建议只在绝对必要再考虑使用。但有的优化后的JavaScript引擎如V8会尝试回收被闭包占用的内存。

闭包缺点:做用域链的这种配置机制引出了一个反作用即闭包只能取得包含函数中任何变量的最后一个值。由于闭包保存的是整个变量对象,而不是某个特殊的变量。

function createFunctions(){
  var result=new Array();
  for(var i=0;i<3;i++){
     result[i]=function(){
        return i;
     };
  }
  return result;
}
createFunctions()[0]();// 3
createFunctions()[1]();// 3
createFunctions()[2]();// 3

当执行 createFunctions 时,它的活动对象里有 arguments=[] , result=undefiend , i=undefiend ,执行完 createFunctions 后, result=[function(){return i},function(){return i},function(){return i}],i=3 ;当此时执行 result 数组时,访问到的i的值老是为3,由于沿着 function(){return i;} 的做用域链查找变量,在外层函数的活动对象上找到i老是为3。数组中每一个函数的做用域链中都保存着 createFunctions 的活动对象,因此这些函数们引用的都是同一个活动对象,同一个变量i。

解决方案:要的就是当时执行时的变量i,那么当时把这个i临时保存一下就能够了,可是保存在哪呢?将i保存在 function(){return i;} 的活动对象中,怎么保存呢?传给 arguments 就行了,只传进来还不行

function createFunctions(){
  var result=new Array();
  for(var i=0;i<3;i++){
     result[i]=function(i){
        return i;
     };
  }
  return result;
}

createFunctions()[0]();// undefiend

由于访问i的时候先从本身所在函数的执行环境的活动对象搜索起,找到i发现 i=undefiend 有值就中止向上搜索了。问题就出在上一步中将i保存在活动对象中, result[i]=function(i){return i;} 这句的执行并无给匿名函数传参,这只是表达式的赋值操做,又不是执行匿名函数。因此如今须要的就是经过某种方式去执行函数的操做把i的值当实参传进去,简单!在匿名函数外部加一层当即执行的匿名函数(这也增长了一层做用域了)。

function createFunctions(){
  var result=new Array();
  for(var i=0;i<3;i++){
     result[i]=(function(i){
         return function(){
              return i;
          }
       })(i);
   }
  return result;
}

createFunctins()[0]();// 0

 

this对象

 this 对象是在运行时基于函数的执行环境绑定的:

  1. 全局函数中, this 等于 window 
  2. 函数被做为某个对象的方法调用时, this 等于那个对象
  3. 匿名函数的执行环境具备全局性, this 指向 window 
  4. 经过 call() 或 apply() 改变函数执行环境的状况下, this 就会指向其余对象。

因为闭包编写的方式不一样, this 的表现:

var name="the window";

var obj={
   name:"the obj",
   getNameFunc:function(){
       //console.log(this==obj);
       return function(){
         console.log(this.name);
     }
  }
}

obj.getNameFunc()();// the window

 obj.getNameFunc() 返回了一个新函数,而后在再全局环境中执行该函数。为何匿名函数没有取得其包含做用域(外部做用域)的 this 对象呢?每一个函数在被调用时,都会自动得到两个特殊的变量: this (建立做用域时得到)和 arguments (建立活动对象得到),内部函数在搜索这两个变量时,只会搜索到本身的活动对象为止,所以永远不可能直接访问外部函数的这两个变量。不过把外部函数做用域的 this 保存在一个闭包可以访问到的变量里就可让闭包访问该对象了。 

下面几种状况特殊的 this :

var name="the window";
var obj={
   name:"the obj",
   getName:function(){
       return this.name;
   }
};

obj.getName();// "the obj"
(obj.getName)();// "the obj"
(obj.getName=obj.getName)();// "the window" 

 第一个是直接调用,第二个是调用后当即执行的表达式,第三个是执行了一条赋值语句,而后再调用返回的结果,赋值语句的返回了一个函数,而后全局环境下调用这个函数,见下图

 

模仿块级做用域

function outputNumber(count){
  for(var i=0;i<count;i++){
     console.log(i);
  }
  var i;// 只变量提高,到后面执行代码步骤时候略过此
  console.log(i);
}

outputNumber(3);// 0 1 2 3

JavaScript不会告诉你是否屡次声明了同一个变量,遇到这种状况,它只会对后续的声明视而不见(不过它会执行后续声明中的变量初始化)。

匿名函数能够用来模仿块级做用域(私有做用域),语法以下:

(function(){
  //这里是块级做用域
})();

以上代码定义并当即调用了一个匿名函数,将函数声明包含在一对圆括号中,表示它其实是一个函数表达式。对于这种语法的理解:

var count=5;
outputNumbers(count);

这里初始化了变量 count 将其值设为5。可是这里的变量是没有必要的,由于能够把值直接传给函数  outputNumbers(5);  这样作之因此可行,是由于变量不过是值的另外一种表现形式,所以用实际的值替换变量没有问题。

var someFunc=function(){
   //这里是块级做用域 
};
someFunc(); 

既然可使用实际的值来取代变量 count ,那这里也用实际的值替换函数名。

function(){
  //这里是块级做用域
}(); 

然而会报错,是由于JavaScript将 function 关键字看成一个函数声明的开始,而函数声明后面不能跟圆括号。可是函数表达式后面能够圆括号,这也就是为何这样能够执行

var someFunc=function(){
 //这里是块级做用域
}();

要将函数声明转化为函数表达式,

(function(){
   //这里是块级做用域
})();
function outputNumber(count){
  (function(){
     for(var i=0;i<count;i++){
       console.log(i);
     }
  })();
   console.log(i);// 报错
}

outputNumber(3);// 0 1 2

在 for 循环外边加了一个私有做用域,在匿名函数中定义的任何变量都会在执行结束时被销毁。在私有做用域中访问变量 count ,是由于这个匿名函数是一个闭包,它能访问包含做用域的全部变量。这种技术常常在全局做用域中被用在函数外部从而限制向全局做用域中添加过多的变量和函数。这种作法还能够减小闭包占用内存问题,由于没有指向匿名函数的引用,只要函数执行完毕,就能够当即销毁其做用域链了。

 

私有变量

严格来说,JavaScript中没有私有成员的概念,全部对象属性都是公有的。不过有私有变量的概念,任何在函数中定义的变量,均可认为是私有变量,由于不能在函数外部访问这些变量。私有变量包括函数的参数,局部变量,在函数内定义的其余函数。若是在函数内部建立一个闭包,那么闭包经过本身的做用域也能够访问这些变量。利用这一点建立用于访问私有变量的公有方法。

把有权访问私有变量和私有函数的方法叫特权方法(privileged method)。

  • 两种在自定义对象上建立特权方法的方式
  1. 在构造函数中定义特权方法
    function MyObject(){
       //私有变量和私有函数
       var privateVariable=10;
       function privateFunction(){
          return false;
      } 
      // 特权方法
      this.publicMethod=function(){
         privateVariable++;
         return privateFunction();
      };
    
    }

    new MyObject();

     这个模式在构造函数内部定义了全部私有变量和函数,又继续建立了可以访问这些私有成员的特权方法。能在构造函数中定义特权方法是由于特权方法做为闭包有权访问在构造函数中定义的全部变量和函数。对这个例子而言,变量 privateVariable 和方法 privateFunction 只能经过特权方法 publicMethod 访问。在建立 MyObject 实例后除了使用 publicMethod() 这一途径外没任何办法能够直接访问私有变量和函数。
    利用私有和特权成员,能够隐藏那些不该该被直接修改的数据

    function Person(name){
      this.getName=function(){
         return name;
      };
      this.setName=function(value){
         name=value;
      };
    }
    
    var p1=new Person("aa");
    p1.getName();// "aa"
    
    var p2=new Person("bb");
    p2.getName();// "bb"
    
    p1.getName();// "aa"

    以上方法定义两个特权方法,在 Person 构造函数外部没有任何办法直接访问 name ,因为这两个方法是在构造函数内部定义的,它们做为闭包可以经过做用域链访问 name 。私有变量 name 在每一个 Person 实例都不相同,这么说吧,每次调用构造函数都会从新建立这两个方法, p1.getName 和 p2.getName 是不一样的函数,虽然调用的是内存中同一个 Person 函数。但 new 构造新实例的步骤是:先建立新实例对象;再在该实例上调用 Person 函数初始化做用域及做用域链 this 等;再添加属性等。无论换成是

    var o1={},o2={};
    Person.call(o1,'aa');
    Person.call(o2,'bb');
    o1.getName();// "aa"

    仍是换成

    function Person(obj,name){
      obj.getName=function(){
         return name;
      };
      obj.setName=function(value){
         name=value;
      };
    }
    var o1={},o2={};
    Person(o1,"aa");
    Person(o2,"bb");
    o1.getName();// "aa"

    都调用了两次 Person ,由于每次调用 Person 就会初始化 Person 的做用域,因此 p1.getName 和 p2.getName 所处的外围做用域是不同的(以前还认为由于是调用了内存中同一个 Person ,觉得 p1.getName 和 p2.getName 有同一个外围做用域,没考虑到每次调用函数实例都会从新初始化做用域)。
    缺点:在构造函数中定义特权方法要求你必须使用构造函数模式来达到这个目的。构造函数模式的缺点是针对每一个实例都会建立同一组新方法,使用静态私有变量来实现特权方法就能够避免这个问题。

  2.  静态私有变量

     经过在私有做用域中定义私有变量和函数,也能够建立特权方法。基本模式以下:

    (function(){
       //私有变量和私有函数
       var privateVariable=10;
       function privateFunction(){
           return false;
       }
       //构造函数
       MyObject=function(){};
       //公有/特权方法
       MyObject.prototype.publicMethod=function(){
           privateVariable++;
           return privateFunction();
      };
    })();

    这个模式建立了个私有做用域,并在其中封装了一个构造函数和相应方法。公有方法在原型上定义,这一点体现典型原型模式。注意到这个模式在定义构造函数时并没使用函数声明,而是使用函数表达式,由于函数声明只能建立局部函数,咱们也没有在声明 MyObject 时使用 var 关键字,就是想让它成为一个全局变量,可以在私有做用域以外被访问。但严格模式下未经声明的变量赋值会致使出错。能够修改成

    'use strict';
    var MyObject;
    (function(){
       //私有变量和私有函数
       var privateVariable=10;
       function privateFunction(){
           return false;
       }
       //构造函数
       MyObject=function(){};
       //公有/特权方法
       MyObject.prototype.publicMethod=function(){
           privateVariable++;
           return privateFunction();
      };
    })();

    其实我以为不用当即执行的匿名函数也能够实现这种在私有做用域中定义私有变量函数的模式,只要把这些放在一个函数中就能够了,而后再执行这个函数。

    function staticFunction(){
       //私有变量和私有函数
       var privateVariable=10;
       function privateFunction(){
           return false;
       }
       //构造函数
       MyObject=function(){};
       //公有/特权方法
       MyObject.prototype.publicMethod=function(){
           privateVariable++;
           return privateFunction();
      };
    }
    
    staticFunction();

    -----分割线----
    这种模式在与构造函数中定义特权方法的主要区别就在于私有变量和函数是由实例共享的由于只调用了即只初始化了一次父环境(意思就是 p1.getName 和 p2.getName 所在的父环境都是同一个,不像构造函数模式中那样拥有各自父环境)。因为特权方法是在原型上定义的,所以全部实例都使用同一个函数。而这个特权方法做为一个闭包老是保存着对包含做用域的引用。

    (function(){
       var name="";
       Person=function(value){
          name=value;
       };
       Person.prototype.getName=function(){
          return name;
       };
       Person.prototype.setName=function(value){
          name=value;
       };
    })();
    
    var p1=new Person("aa");
    p1.getName();// "aa"
    var p2=new Person("bb");
    p2.getName();// "bb"
    p1.getName();// "bb"

     Person 构造函数和 getName 和 setName 都有权访问私有变量 name 。在这种模式下,变量 name 就成了一个静态的,由全部实例共享的属性。在一个实例上调用 setName() 会影响全部实例。
    以这种模式建立静态私有变量会由于使用原型而增进代码复用,但每一个实例都没有本身的私有变量。
    多查找做用域链中的一个层次,就会在必定程度上影响查找速度,这正是使用闭包和私有变量的一个不足之处。

  • 模块模式:前面的模式用于为自定义类型建立私有变量和特权方法。道格拉斯所说的模块模式则是为单例建立私有变量和特权方法,所谓单例(singleton)指的就是只有一个实例的对象,JavaScript是以对象字面量的方式来建立单例对象的。
    var singleton={
      name:value,
      method:function(){
         //这里是方法的代码
      }
    };
    模块模式经过为单例添加私有变量和特权方法可以使其获得加强。语法以下:
    var singleton=function(){
       //私有变量和私有函数
       var privateVariable=10;
       function privateFunction(){
           return false;
       }
       //特权/公有方法和属性
       return {
          publicProperty:true,
          publicMethod:function(){
              privateVariable++;
              return privateFunction();
          }
       } 
    }();
    这个模式使用了一个返回对象的匿名函数,将一个对象字面量做为函数返回。本质上这个对象字面量定义的是一个单例的公共接口。这种模式在须要对单例进行某些初始化同时又须要维护其私有变量时是很是有用的。
    var application=function(){
       //私有变量和函数
       var components=new Array();
       //初始化
       components.push(new BaseComponent());
       //公共
       return {
          getComponentCount:function(){
              return components.length;
          },
          registerComponent:function(component){
              if(typeof component=="object"){
                  components.push(component);
              }
          }
       }
    }();
    在web应用程序中,常常须要使用一个单例来管理应用程序级的信息。若是必须建立一个对象并以某些数据对其进行初始化,同时还要公开一些可以访问这些私有数据的方法,那就可使用模块模式。这种模式建立的每一个单例都是 Object 的实例。
  • 加强的模块模式:若是想让单例是某种类型的实例,改进了模块模式,在返回对象以前加入对其加强的代码。同时还必须添加某些属性和方法对其加以加强。
    var singleton=function(){
      //私有变量和私有函数
      var privateVariable=10;
      function privateFunction(){
        return false;
      }
      //建立对象
      var obj=new CustomType();
      //添加特权/公有属性和方法
      obj.publicProperty=true;
      obj.publicMethod=function(){
          privateVariable++;
          return privateFunction();
      }
      return obj;
    }();
    var application=function(){
      //私有变量和函数
      var components=new Array();
      //初始化
      components.push(new BaseComponent());
      //建立application的一个局部版本
      var app=new BaseComponent();
      app.getComponentCount=function(){
         return components.length;
      }; 
      app.registerComponent=function(component){
         if(typeof component=="object"){
             components.push(component);
         }
      };
      return app;
    }();

     

 

 闭包的做用总结:

  1. 使用闭包能够在JavaScript中模仿块级做用域
    建立并当即调用一个函数,这样既能够执行其中代码,又不会在内存中留下对该函数的引用。结果就是函数内部的全部变量都会被当即销毁除非将某些变量赋值给了包含做用域中的变量
  2. 闭包还可用于在对象中建立私有变量
    经过闭包来实现公有方法,经过公有方法能够访问在包含做用域中定义的变量。有权访问私有变量的公有方法叫特权方法。可使用构造函数模式,原型模式来实现自定义类型的特权方法,使用模块模式,加强的模块模式实现单例的特权方法。

 

参考:
《JavaScript高级程序设计》

深刻解读JavaScript面向对象编程实践

JS核心系列:漫谈JS引擎的运行机制

相关文章
相关标签/搜索