JS的函数篇(4.3W字)

本系列将从如下专题去总结:javascript

1. JS基础知识深刻总结
2. 对象高级
3. 函数高级
4. 事件对象与事件机制html

暂时会对以上四个专题去总结,如今开始Part3:函数高级。下图是我这篇的大纲。 java

js函数学习大纲

3.1 this的使用总结

this是在函数执行的过程当中自动建立的一个指向一个对象的内部指针。确切的说,this并非一个对象,而是指向一个已经存在的对象的指针,也能够认为是this就是存储了某个对象的地址。面试

this的指向不是固定的,会根据调用的不一样,而指向不一样的地方。 此章节的讲解思路是这样的: chrome

this总结

3.1.1 搞懂this指向的预备知识

  • 对在全局做用域中定义的变量和函数的进一步认识编程

    永远记住:只要是在全局做用域声明的任何变量和函数默认都是做为window对象的属性而存在的.json

    理解完以上的这句话,咱们在来讲明一下其中的区别,这是不少人没有关注过的。数组

    console.log(window.a);  //undefined
    console.log(a);   //报错!a is not defined 
    复制代码

    注解:也就是在未对变量(a)进行声明时,就会出现以上结果。首先明确一点,就是全局变量awindow的属性。因此,咱们从这里就能够发现,何时undefined,何时报错?——那就是若是是访问一个对象的属性时,它没有声明赋值,那就是undefined;若是访问一个变量,它没有声明赋值,那就是报错。promise

    好,如今回头过来,咱们看全局做用域变量和函数的认识。浏览器

    <script type="text/javascript">
          var num = 10;      //全局做用域声明的变量
          function sum () {  //全局做用域声明的函数
              alert("我是一个函数");
          }
          alert(window.num);  // 10
          window.sum();       // 我是一个函数
          // 在调用的时候window对象是能够省略的。
       </script>
    复制代码
  • 构造函数和非构造函数的澄清

    在JavaScript中构造函数和非构造函数没有本质的区别。惟一的区别只是调用方式的区别。

    • 使用new 就是构造函数
    • 直接调用就是非构造函数

    看一个示例代码:

    <script type="text/javascript">
          function Person () {
              this.age = 20;
              this.sex = "男";
          }
          //做为构造函数调用,建立一个对象。 这个时候实际上是给p添加了两个属性
          var p = new Person();
          alert(p.age + " " + p.sex);
    
          //做为普通函数传递,实际上是给 window对象添加了两个属性 
          //任何函数本质上都是经过某个对象来调用的,若是没有直接指定就是window,也就是Window可省略
          Person();
          alert(window.age + " " + window.sex);
      </script>
    复制代码

3.1.2 第一个方向:全局做用域中的this指向

全局做用域中使用this,也就是说不在任何的函数内部使用this,那么这个时候this就是指的 Window

<script type="text/javascript">
	//全局做用域中的this
    //向this对象指代的对象中添加一个属性 num, 并让属性的值为100
    this.num = 100;
    // 由于this就是window,因此这时是在修改属性num的值为200
    window.num = 200;
    alert(this === window);  // true this就是指向的window对象,因此是恒等 
    alert(this.num);    //200   
    alert(window.num);  //200
</script>
复制代码

3.1.3 第二个方向:函数中的this

函数中this又可分为构造函数和非构造函数的this两个概念去理解。

  • 非构造函数中的this指向

    非构造函数中this指向的就是 调用这个方法的那个对象

示例1:

<script type="text/javascript">
      function test() {
          alert(this == window);
          this.age = 20;
      }
      test();  //实际上是 window.test();  因此这个时候test中的this指向window
  </script>
复制代码

示例2:

<script type="text/javascript">
      var p = {
          age : 20,
          sex : "男",
          sayAge: function (argument) {           
              alert(this.age);
          }
      }
      p.sayAge(); //调用对象p的方法sayAge()  因此这个时候this指的是 p 这个对象
  </script>
复制代码

示例3:

<script type="text/javascript">
      var p = {
          age : 20,
          sex : "男",
          sayAge: function (argument) {
              alert(this.age);
              alert(this === p);  //true
          }
      }
      var again = p.sayAge;   //声明一个变量(方法),把p的方法复制给新的变量
//调用新的方法: 实际上是window.again(). 
//因此 方法中的this指代的是window对象,这个时候age属性是undefined
// this和p也是不相等的。 
      again();    
  </script>
复制代码

综上:this的指代和代码出现的位置无关,只和调用这个方法的对象有关。

  • 构造方法中的this指向

构造方法中的this指代的要将来要建立的那个对象。

示例1:

<script type="text/javascript"> 
      function Person () {
          this.age = 20;
          return this;  //做为构造函数的时候,这个行代码默认会添加
      }
      var p1 = new Person();  //这个时候 Person中的this就是指的p1
      var p2 = new Person();  //这是时候 Person中的this就是知道p2
  </script>
复制代码

多了解一点:其实用new调用构造函数的时候,构造函数内部其实有个默认的return this; 这就是为何this指代那个要建立的对象了。

3.1.4 第三个方向: 改变this的指向(显式绑定)

在JavaScript中,容许更改this的指向。 经过call方法或apply方法

函数A能够成为指定任意对象的方法进行调用 。函数A就是函数对象,每一个函数对象中都有一个方法call,经过call可让你指定的对象去调用这个函数A。

ECMAScript 规范给全部函数都定义了callapply 两个方法。 callapply是放在Function的原型对象上的,而不是Object原型对象上!

<script type="text/javascript"> 
    var age = 20;
    function showPropertyValue (propertyName) {
        alert(this[propertyName]);
    }
    //使用call的时候,第一个参数表示showPropertyValue中的this的执行,后面的参数为向这个函数传的值。
    //注意一点:若是第一个参数是null,则this仍然是默认的指向。
    showPropertyValue.call(null, "age");
    showPropertyValue.call(this, "age");
    showPropertyValue.call({age:50}, "age")
</script>
复制代码

3.1.5 call / apply / bind 的详解

this的指向中有第三个方向就是经过call/apply去改变this的指向,这个JavaScript中一个独特的使用形式,其余语言并无。那么,咱们就在这里顺带讲一下callapply 以及bind的用法。

本小节将从三个方面讲解: 1:applycall的区别 2:applycall的用法 3:callbind的区别

3.1.5.1 apply 和 call 的区别

ECMAScript 规范给全部函数都定义了 callapply 两个方法,它们的应用很是普遍,它们的做用也是如出一辙,只是传参的形式有区别而已。

简单来讲,假设有一个函数A,咱们调用函数A会直接去A(),那么若是是A()这样直接调用的话,函数体A里面的this就是window了。而咱们能够经过call(或apply)去调用,好比:A.call().这样子调用就能够指定A中的this究竟是哪一个对象。

call来作比对,里面有两个参数,参数一就是从新指定其中的this是谁,参数2是属性名。而事实上,callapply也就是参数二的不一样这个差别。

apply apply 方法传入两个参数:一个是做为函数上下文的对象,简单来讲,从新指定函数中的this是谁。另一个是做为函数参数所组成的数组,是传入一个数组。

var obj = {
    name : 'ya LV'
}

function func(firstName, lastName){
    console.log(firstName + ' ' + this.name + ' ' + lastName);
}

func.apply(obj, ['A', 'B']);    // A ya LV B
复制代码

能够看到,obj是做为函数上下文的对象,也就是说函数functhis指向了 obj这个对象。原本若是直接调用func(),那么函数体中的this就是指的是window。可是如今有了参数一,就是从新指定this,这个this就是参数一的obj这个对象。参数 A 和 B 是放在数组中传入 func函数,分别对应 func 参数的列表元素。

call call方法第一个参数也是做为函数上下文的对象。与apply没有任何区别。可是后面传入的是一个参数列表,而不是单个数组。

var obj = {
    name: 'ya LV'
}

function func(firstName, lastName) {
    console.log(firstName + ' ' + this.name + ' ' + lastName);
}

func.call(obj, 'C', 'D');       // C ya LV D
复制代码

对比apply咱们能够看到区别,C 和 D 是做为单独的参数传给 func 函数,而不是放到数组中。

对于何时该用什么方法,其实不用纠结。若是你的参数原本就存在一个数组中,那天然就用apply,若是参数比较散乱相互之间没什么关联,就用 call

补充一个使用 apply 的例子。好比求一个数组的最大值?

明确JavaScript中没有返回一个数组中最大值的函数。可是,有一个函数Math.max能够返回任意多个数值类型的

参数中的最大值,Math.max函数入参并不支持数组,只能是将多个参数逐个传入,用逗号分隔。

这个时候若是要能够用Math.max函数,且传参数能够是一个数组。咱们天然而然会想到全部函数都定义了callapply的方法,咱们能够配合applycall来实现,又由于call传参数并非一个数组。全部咱们就选择出Math.max函数加上apply就能够实现咱们的目的。

本来只能这样用,并不能直接用数组,见示例:

let max = Math.max(1, 4, 8, 9, 0)
复制代码

有了 apply,就能够这么调用:

let arr = [1, 4, 8, 9, 0];
let max = Math.max.apply(null, arr);
复制代码

在调用apply的时候第一个参数给了一个null,这个是由于没有对象去调用这个方法,咱们只须要用这个方法帮咱们运算,获得返回的结果就行,因此就直接传递了一个null过去。

3.5.1.2 apply 和 call 的用法

applycall的用法能够分为三个:改变this的指向,借用别的对象的方法,调用函数。

  • 1.改变 this 指向
var obj = {
      name: 'ya LV'
  }

  function func() {
      console.log(this.name);
  }

  func.call(obj);       // ya LV
复制代码

这个在前一小节有讲到,因此咱们就简单的再来看看。所谓“熟能生巧”,同样东西,一个知识点,每看一次会有不一样的体会,可能此次看的过程让你有更深入的思考,这就是进步。call方法的第一个参数是做为函数上下文的对象,这里把 obj做为参数传给了func,此时函数里的this便指向了obj对象。此处func 函数里其实至关于:

function func() {
      console.log(obj.name);
  }
复制代码

另外注意下call的一些特别用法,很奇葩的this指向。稍微注意下,有点印象就好。

function func() {
      console.log(this);
    }
    func.call();   //window
    func.call(undefined);     //window
    func.call(null);    //window
    func.call(1);  //Number {1} 这种状况会自动转换为包装类Number 就至关于下面一行代码
    func.call(new Number(1));   //Number {1}
复制代码
  • 2.借用别的对象的方法
var Person1  = function () {
      this.name = 'ya LV';
  }

  var Person2 = function () {
      this.getname = function () {
          console.log(this.name);
      }
      Person1.call(this);
  }
  var person = new Person2();
  person.getname();       // ya LV
复制代码

从上面咱们看到,Person2 实例化出来的对象 person 经过 getname 方法拿到了 Person1中的 name。由于在 Person2中,Person1.call(this) 的做用就是使用Person1 对象代替 this 对象,那么 Person2 就有了Person1 中的全部属性和方法了,至关于 Person2继承了Person1的属性和方法。 不理解的话咱们再来慢慢看,咱们说A.call ( 参数一)这样的形式就是从新指定函数A中的this‘参数一’这个对象,那么咱们来看看Person2函数体中的Person1.call(this)这条语句,其中这条语句的this是指Person2这个对象。如今就是把Person1函数的this从新指向为Person2,是否是有了Person2.name='ya LV'

  • 3.调用函数

applycall 方法都会使函数当即执行,所以它们也能够用来调用函数。这个咱们在这节的一开始就有说,好比A()A.call()都是调用函数A。

function func() {
      console.log('ya LV');
  }
  func.call();            // ya LV
复制代码

3.5.1.3 call 和 bind 的区别

EcmaScript5 中扩展了叫 bind 的方法,在低版本的 IE 中不兼容。它和call很类似,接受的参数有两部 分,第一个参数是是做为函数上下文的对象,第二部分参数是个列表,能够接受多个参数。

它们之间的区别有如下两点。

  1. 区别1.bind 的返回值是函数
var name='HELLO'
    var obj = {
      name: 'ya LV'
    }

    function func() {
      console.log(this.name);
    }
    
    //将func的代码拷贝一份,而且永远改变其拷贝出来的函数中的this,为bind第一个参数所指向的对象。把这 份永远改变着this指向的函数返回给func1.
    var func1 = func.bind(obj);
   //bind方法不会当即执行,是返回一个改变上下文this的函数,要对这个函数调用才会执行。
    func1();  //ya LV
   //能够看到,如今这份改变this以后拷贝过来的函数,this的指向永远是bind()绑定的那个,无论以后去call 从新指向对象,func1 都不会改变this的指向。永远!可知,bind比call优先级还高。
    func1.call({name:'CALL'});   //ya LV

    //又从func从新拷贝一份永远改变this指向对象为{name:'LI SI'}这个对象的函数,返回给func2.
    var func2 = func.bind({name:'LI SI'});
    func2();   //LI SI

   //注意,这里是拷贝一份func2(而不是func)的代码,而func2以前已经绑定过去永远改变this的指向了,因此这 里并不去改变!仍是会输出原来的最早bind的this指向对象。
    var func3 = func2.bind({name:'ZHANG SAN'});
    func3();   //LI SI

   //上面对func最初的函数进行了屡次绑定,绑定后原函数 func 中的 this 并无被改变,依旧指向全局对象 window。由于绑定bind的过程是拷贝代码的一个过程,而不是在其自身上修改。window.name = HELLO
    func();   //HELLO
复制代码

bind 方法不会当即执行,而是返回一个改变了上下文this后的函数。而原函数func中的 this并无被改变,依旧指向全局对象 window

  1. 区别2.参数的使用
function func(a, b, c) {
      console.log(a, b, c);
  }
  var func1 = func.bind(null,'yaLV');

  func('A', 'B', 'C');            // A B C
  func1('A', 'B', 'C');           // yaLV A B
  func1('B', 'C');                // yaLV B C
  func.call(null, 'yaLV');       // yaLV undefined undefined
复制代码

call 是把第二个及之后的参数做为 func方法的实参传进去,而 func1方法的实参实则是在bind中参数的基础上再日后排。也就是说,var func1 = func.bind(null,'yaLV'); bind现有两个参数,第一个是指向,第二个实参是'yaLV',那么就是先让func中的a='yaLV',而后没排满就是让func1('A', 'B', 'C'); 这个参数依次排,如今b='A'c='B' , 形参已经排完了。也就是输出yaLV A B

在低版本浏览器没有bind方法,咱们也能够本身实现一个。

if (!Function.prototype.bind) {
   Function.prototype.bind = function () {
       var self = this,                        // 保存原函数
       context = [].shift.call(arguments), // 保存须要绑定的this上下文
       args = [].slice.call(arguments);    // 剩余的参数转为数组
       return function () {                    // 返回一个新函数
       self.apply(context[].concat.call(args[].slice.call(arguments));
              }
          }
      }
复制代码

3.1.6 习题与案例

习题1

<script type="text/javascript">
     var name='window_dqs';
     var obj={
        name:'obj_dqs',
        showName:function(){
            console.log(this.name);
        }};

     function fn(){
        console.log(this);
     }
     function fn2(){
        this.name='fn_dqs';
     }
	
	//由于obj去调用,this就是obj
     obj.showName();    //obj_dqs  
     //由于借调,而此时借调的对象是this,而this在全局做用域上就是指window,因此找window.name
     obj.showName.apply(this);   //window_dqs
     //由于借调的对象是一个函数对象,那么this就是指函数对象,this.name就是函数名
     obj.showName.apply(fn2);    //fn2
</script>
复制代码

习题2

<script type="text/javascript">
 var name='window_dqs';
     function fn(){
        this.name='fn_dqs';
        this.showName=function(){
            console.log(this.name);
        }
        console.log(this);
     }

     function fn2(){
        this.name='fn_pps';
        this.showName=function(){
            console.log(this.name);
        }
        console.log(this);
     }

     var p=new fn();
     fn2.apply(p);
     p.showName();

     var obj={};
     fn2.apply(obj);
     obj.showName();
</script>
复制代码

结果是:

var p=new fn();输出fn { name: 'fn_dqs', showName: [Function] }
    fn2.apply(p);输出fn { name: 'fn_pps', showName: [Function] }
    p.showName();输出fn_pps

	var obj={};
    fn2.apply(obj);输出Object{name: "fn_pps"showName: ƒ ()__proto__: Object··}
    obj.showName();输出fn_pps
复制代码

习题3

<script type="text/javascript">
var name='window_dqs';
     var obj={
        name:'json_dqs',
        showName:function(){
            console.log(this.name);
            return function(){
                console.log(this.name);
            }
        }
     }
    var p=obj.showName();
    obj.showName()();
    p.call(obj);
</script>
复制代码

结果是:

json_dqs
json_dqs
window_dqs
json_dqs
复制代码

面试题1

代码片断1:

var name = "The Window";
  var object = {
    name: "My Object",
    getNameFunc: function (){
      return function (){
        return this.name;
      };
    }
  };
  console.log(object.getNameFunc());     //ƒ (){return this.name;}
  console.log(object.getNameFunc()());  //The Window
复制代码

代码片断一没有闭包。有嵌套,但没有用外部函数的变量或函数。是使用this的。this与调用方式有关。

理解:看object.getNameFunc()是对象.方法() 返回的是一个函数,这个函数还未执行。js中this是动态的,因此函数没有执行,并不肯定函数里的this是指的是谁?那么如今再对返回的函数加个(),也就是object.getNameFunc()(),调用执行,把最后一个括号和最后一个括号前当作两个部分,前面是函数名,后面一个括号是调用。至关于test(),这个时候this就是window。故这样调用的函数this就是指的window,故window.name=The Window.

代码片断二: 对于片断一咱们的本意是否是想输出My Object。那么怎么改造,经过that=this去操做。

var name2 = "The Window";
  var object2 = {
    name2: "My Object",
    getNameFunc: function () {
      var that = this;  //缓存this
      return function () {
        return that.name2;
      };
    }
  };
  console.log(object2.getNameFunc());    //ƒ (){return that.name2;}
  console.log(object2.getNameFunc()()); //My Object
复制代码

代码片断二是有闭包的,有嵌套函数。内部函数有使用外部函数的变量that。外部和内部函数有执行。

理解:首先仍是看object2.getNameFunc()返回一个函数,注意这个函数中没有this,在调用object2.getNameFunc时,咱们有执行一句var that = this;也就是把thisthat,这个时候this是指的是object2。再次调用object2.getNameFunc()()时就是执行object2.getNameFunc()返回来的函数”。that.name2=object2.name2;实质上是闭包,使用了外部函数的that变量。

代码片断三(对片断二的改造):

var name3 = "The Window";
  var object3 = {
    name3: "My Object",
    getNameFunc: function () {
      return function () {
        return this.name3;
      }.bind(this);
    }
  };
  console.log(object3.getNameFunc());     //ƒ (){return this.name3;}
  console.log(object3.getNameFunc()());   //My Object
复制代码

理解:与“代码片断二”同样,只是片断二是经过that=this去改变this的值,而片断三是经过bind绑定this的值。看bind(this)这里的this就是指这条语句object3.getNameFunc()调用的对象object3.因此经过这个手段去把this指向了前面的对象object3.再去调用返回的函数时,那么this.name3=object3.name3

面试题2

<script>
var myObject = {
    foo: "bar",
    func: function() {
        var self = this;
        console.log(this.foo);  //bar
        console.log(self.foo);  //bar
        (function() {
          console.log(this.foo);  //undefined 此时的this是window
          console.log(self.foo);  //bar 闭包能够看到外部的局部变量
        }());  //匿名函数自执行,是window上调用这个函数。
    }
};
myObject.func();

//那么如何修改呢?使得在自执行函数中的this.foo就是咱们想要的bar呢?
//提供两种方法:
//case1:用call去指向this是谁
var myObject = {
    foo: "bar",
    func: function() {
        var self = this;
        console.log(this.foo); // bar
        console.log(self.foo); // bar
        (function() {
            console.log(this.foo);  // bar
            console.log(self.foo); // bar
        }.call(this));  
        //myObject.func();这样调用func(),那么func()中的this就是前面的对象myObject。
    }
};
myObject.func();

//case2:用bind去绑定this,但要注意bind是返回一个函数,故要bind(this)(),后一个括号表示函数调用。把bind(this)将拷贝一份并改变this的指向的函数执行。
var myObject = {
    foo: "bar",
    func: function() {
        var self = this;
        console.log(this.foo);  // bar
        console.log(self.foo);  // bar
        (function() {
            console.log(this.foo);  // bar
            console.log(self.foo);  // bar
        }.bind(this)());
    }
};
myObject.func();
</script>
复制代码

面试3 考察this的指向: 难点:数组(类数组)中的元素当作函数调用时的this指向 也就是,若是是调用数组(类数组)中的元素, 元素函数中的this是这个数组(类数组)。

<script>
var length = 10;
function fn(){
    console.log(this.length);
}
var obj = {
    length: 5,
    method: function (fn){  // [fn, 1, 2]  
        fn();  // 10
        arguments[0]();  // 3
    }
};
obj.method(fn, 1, 2);
/*obj.method(fn, 1, 2);传实参fn过去,此时fn拿到函数的地址值拷贝给形参fn,在执行fn()这里调用是至关window调用fn,this指的是window。而不是obj不要感受是在obj里面就是,迷惑你们的。this的指向永远跟调用方式有关。
*另外,arguments[0]();调用时,这个时候是类数组中的元素调用,那么这时的this是类数组自己,因此,数组.length是否是输出类数组的长度。
*若是是调用数组(类数组)中的元素, 元素函数中的this是这个数组(类数组).为何呢?看如下两个例子:
* */

//例子1:
var obj = {
    age : 100,
    foo : function (){
        console.log(this);
    }
}
var ff = obj.foo;
ff();  //window
obj.foo(); //{age: 100, foo: ƒ}
obj["foo"]();  //{age: 100, foo: ƒ}
//上面的这个例子没有问题吧。很天然的。

//例子2:
var arr = [
    function (){
        console.log(this);
    },function (){

    }
];
var f = arr[0];
f();  //window

/*arr.0()--相似于这么写把,只是数组不容许这样的语法--*/
 arr[0]();  //输出数组自己:(2) [ƒ, ƒ] 。故验证一句话:若是调用数组(类数组)中的元素时,那么这时的this是数组(类数组)自己。
</script>
复制代码

3.2 原型与原型链

一个特别经典的总结:

a.b就能够看出做用域与做用域链,原型与原型链的知识。 (详见Part1的1.1.3)

3.2.1 五张图理解原型与原型链

构造函数建立对象咱们先使用构造函数建立一个对象:

function Person() {
    
}
var person = new Person();
person.name = 'name';
console.log(person.name) // name
复制代码

在这个例子中,Person就是一个构造函数,咱们使用new建立了一个实例对象person

很简单吧,接下来进入正题:【prototype】

任何的函数都有一个属性prototype,这个属性的值是一个对象,这个对象就称为这个函数的原型对象。可是通常状况,咱们只关注构造函数的原型。好比:

function Person() {

}
// 虽然写在注释里,可是你要注意:prototype是函数才会有的属性
Person.prototype.name = 'name';

var person1 = new Person();  //person1是Person构造函数的实例
var person2 = new Person();  //person2是Person构造函数的实例
console.log(person1.name) // name
console.log(person2.name) // name
复制代码

其实,函数的prototype属性指向了一个对象,这个对象正是调用该构造函数而建立的实例的原型,也就是这个例子中的person1person2的原型。实例实际上是经过一个不可见的属性[[proto]]指向的。

你能够这样理解:每个JavaScript对象(null除外)在建立的时候就会与之关联另外一个对象,这个对象就是咱们所说的原型,每个对象都会从原型”继承”属性。

让咱们用一张图表示构造函数和实例原型之间的关系:

prototype
那么咱们该怎么表示实例与实例原型,也就是 person1 person2Person.prototype之间的关系呢,这时候咱们就要讲到第二个属性: [[proto]]

当使用构造函数建立对象的时候, 新建立的对象会有一个不可见的属性[[proto]], 他会指向构造函数的那个原型对象。事实上,每个JavaScript对象(除了null)都具备的一个不可见属性,叫[[proto]],这个属性会指向该对象的原型。

为了证实这一点,咱们能够在火狐或者谷歌中输入:

function Person() {
    
}
var person1 = new Person();
console.log(person1.__proto__ === Person.prototype); //true
复制代码

因而咱们更新下关系图:

proto
既然实例对象和构造函数均可以指向原型,那么原型是否有属性指向构造函数或者实例呢?指向实例却是没有,由于一个构造函数能够生成多个实例,可是原型指向构造函数却是有的,这就要讲到第三个属性:【 constructor】,每一个原型都有一个 constructor属性指向关联的构造函数。

为了验证这一点,咱们能够尝试:

function Person() {

}
console.log(Person === Person.prototype.constructor); //true
复制代码

因此再更新下关系图:

原型与原型链详解
综上咱们已经得出:

function Person() {

}
var person1 = new Person();
//对象的__proto__属性: 建立对象时自动添加的, 默认值为构造函数的prototype属性值(很重要)
console.log(person1.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true

// 顺便学习一个ES5的方法,能够得到对象的原型
console.log(Object.getPrototypeOf(person1) === Person.prototype) //true
复制代码

了解了构造函数、实例原型、和实例之间的关系,接下来咱们讲讲实例和原型的关系:【实例与原型】。当读取实例的属性时,若是找不到,就会查找与对象关联的原型中的属性,若是还查不到,就去找原型的原型,一直找到最顶层为止。

举个例子:

function Person() {

}
//往Person对象原型中添加一个属性
Person.prototype.name = 'name';
//建立一个person1实例对象
var person1 = new Person();
//给建立的实例对象person1添加一个属性
person1.name = 'name of this person1';
//查找person1.name,由于自己实例对象有,那么就找到了自身实例对象上的属性和属性值
console.log(person1.name) // name of this person1
//删除实例对象的属性和属性值
delete person1.name;
//查找属性name,在实例对象自身上找不到,经过proto指向往原型链上找,在原型对象中找到
console.log(person1.name) // name
复制代码

在这个例子中,咱们设置了person1name属性,因此咱们能够读取到为name of this person1,当咱们删除了person1name属性时,读取person1.name,从person1中找不到就会从person的原型也就是person.__proto__ == Person.prototype中查找,幸运的是咱们找到了为name,可是万一尚未找到呢?原型的原型又是什么呢?

var obj = new Object();
obj.name = 'name'
console.log(obj.name) // name
复制代码

因此原型对象是经过Object构造函数生成的,结合以前所讲的一句很重要的话,几乎就是涵盖原型与原型链知识的始终的一句话,那就是:实例对象的proto指向构造函数的prototype。也就是说,Person.prototype这个原型对象(实例原型)是经过Object这个构造函数new出来的,也就是Person.prototype这个原型对象是Object的实例,因此这个实例会有proto属性指向Object构造函数的原型对象Object.prototype

这里呢插入一句总结出来的话,逆推顺推都是可行的,那就是:实例经过proto这个属性指向其构造函数的原型对象。因此咱们再更新下关系图:

原型与原型链详解
Object.prototype的原型呢? null,嗯,就是 null。因此查到 Object.prototype就能够中止查找了。因此最后一张关系图就是:
原型与原型链详解

那【原型链】是啥 ? 那就是由proto这个属性进行查找的一个方向这就是一条原型链。图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线,都是经过proto属性进行查找的。

​ 那么访问一个对象的属性时,怎么经过原型链去查找属性或方法呢 ? 先在自身属性中查找,找到返回。若是没有, 再沿着proto这条链向上查找, 找到返回。若是最终没找到, 返回undefined

3.2.2 从代码中看原型与原型链

原型与原型链详解
解析上图:首先 n , s 都是全局变量,而后经过对象字面量的方法去建立了一个对象。而后有一个构造函数(之因此是构造函数,是由于后面代码有 new的操做),这个构造函数就会有函数声明提早,当构造函数声明时,就会去在内存中建立一个 person的函数对象,这个函数对象里 只有prototype属性,去指向 person的函数原型对象。要注意,如今尚未去执行里面的代码,只是函数声明时建立了一个 person的函数对象。后面就是new的实例对象,新 new出来的实例对象 P2 P3就会在内存中分配一块内存去把地址值给它,如今才会去执行构造函数中的代码。因此只有 P2 P3才会 有 name age speak属性和方法。这些新 new出来的实例对象就会有一个不可见的属性 proto,去指向这个原型对象。而最终这个 person的函数原型对象会有指向一个 object的原型对象,再上去其实就是 null。这就一层一层往上走就是原型链,由于原型链,咱们才会有继承的特性。

​ 注几点:

  1. 从上图的图示中能够看到,建立 P2 P3 实例对象虽然使用的是 Person 构造函数,可是对象建立出来以后,这个P2 P3 实例对象其实已经与 Person 构造函数(函数对象)没有任何关系了,P2 P3 实例对象的[[ proto ]] 属性指向的是 Person 构造函数的原型对象。
  2. 若是使用 new Person() 建立多个对象,则多个对象都会同时指向 Person 构造函数的原型对象。
  3. 咱们能够手动给这个原型对象添加属性和方法,那么P2 P3 ····这些实例对象就会共享这些在原型中添加的属性和方法。也就是说,原型对象至关于公共的区域,全部的同一类的实例均可以去访问到原型对象。
  4. 若是咱们访问P2实例对象 中的一个属性 gender ,若是在P2 对象中找到,则直接返回。若是 P2 对象中没有找到,则直接去P2对象的 [[proto]] 属性指向的原型对象中查找,若是查找到则返回。(若是原型中也没有找到,则继续向上找原型的原型---原型链)。
  5. 读取对象的属性值时: 会自动到原型链中查找
  6. 设置对象的属性值时: 不会查找原型链, 若是当前对象中没有此属性, 直接添加此属性并设置其值。好比经过P2对象只能读取原型中的属性 name的值,并不能修改原型中的属性name 的值。 P2.gender= "male" ; 并非修改了原型中的值,而是在 P2对象中给添加了一个属性 gender
  7. 方法通常定义在原型中, 属性通常经过构造函数定义在对象自己上。

另外看看,原型与原型链的三点关注:

  1. 函数的显示原型指向的对象默认是空Object实例对象(但Object不知足)
console.log(Fn.prototype instanceof Object) // true
console.log(Object.prototype instanceof Object) // false
console.log(Function.prototype instanceof Object) // true
复制代码
  1. 全部函数都是Function的实例(包含Function)
console.log(Function.__proto__===Function.prototype) //true
复制代码
  1. Object的原型对象是原型链尽头
console.log(Object.prototype.__proto__) // null
复制代码

3.2.3 探索instanceof

instanceof是如何判断的?

  • 表达式: A instanceof B

  • 若是B构造函数的原型对象(B.prototype)在A实例对象的原型链(A.proto.proto·····沿着原型链)上, 返回true, 不然返回false。(见下图)

    instanceof

  • 也就是说A实例对象的原型链上可能会有不少对象,只要B构造函数的原型对象有一个是在其原型链上的对象便可返回true

  • 反过来讲也同样,实例对象A是否能够经过proto属性(沿着原型链,A.proto.proto·····)找到B.prototype(B的原型对象),找到返回true,没找到返回false.

  • 注1:对实例对象的说明,事实上,实例对象有两种。一种是咱们常常说的new出来的实例对象(好比构造函数Person , new出来p1 p2...,这些都是实例对象),另一种就是函数,函数自己也是实例,是Function new出来的。但咱们通常说的实例对象就是指new出来的相似于p1 p2这些的实例对象。

Function是经过new本身产生的实例(Function.proto===Function.prototype)

案例1:

//一个构造函数Foo
  function Foo() {  }
  //一个f1实例对象
  var f1 = new Foo()
  //翻译:f1是Foo的实例对象吗?
  //还记得我说过,一个实例对象经过proto指向其构造函数的原型对象上。
  //深刻翻译:f1这个实例对象经过proto指向是否能够找到Foo.prototype上呢?
  console.log(f1 instanceof Foo) // true
  //这行代码能够得出,沿着proto只找了一层就找到了。
  console.log(f1.__proto__ === Foo.prototype);   // true
  
  //翻译:f1是Object的实例对象吗?
  //深刻翻译:f1这个实例对象经过proto指向是否能够找到Object.prototype上呢?
  console.log(f1 instanceof Object) // true
  //这两行代码能够得出,沿着proto找了两层才找到。事实上,f1.__proto__找到了Foo.prototype(Foo构造函数原型上),再次去.__proto__,找到了Object的原型对象上。见下图。
  console.log(f1.__proto__ === Object.prototype);  // false
  console.log(f1.__proto__.__proto__ === Object.prototype);  // true
复制代码

js经典原型与原型链的图
案例2:

//这个案例的实质仍是那句话:一个实例对象经过proto属性指向其构造函数的原型对象上。
//翻译:实例对象Object是否能够经过proto属性(沿着原型链)找到Function.prototype(Function的原型对象)
  console.log(Object instanceof Function) // true
//以上结果的输出能够看到下图,Object.__proto__直接找到一层就是Function.prototype.(Object created by Function)可知Object构造函数是由Function建立出来的,也就是说,Object这个实例是new Function出来的。

  console.log(Object instanceof Object) // true
//颇有意思。上面咱们已经知道Object这个实例是new Function出来的。也就是Object.proto指向Function.prototype。有意思的是,Function的原型对象又是Object原型对象的一个实例,也就是Function.prototype.proto 指向 Object.prototype .颇有意思吧,见下图很更清楚这个“走向”。


  console.log(Function instanceof Function) // true
//由这个可知,能够验证咱们的结论:Function是经过new本身产生的实例。 Function.proto===Function.prototype

  console.log(Function instanceof Object) // true
//Function.proto.proto===Function.prototype (找了两层)

  //定义了一个Foo构造函数。由下图可知,Foo.proto.proto.proto===null 
  function Foo() {}
  console.log(Object instanceof  Foo) // false
//这条语句要验证的是,Object是否能够经过其原型链找到Foo.prototype。
// Object.proto.proto.proto=null 并不会找到Foo.prototype。因此,返回FALSE。
复制代码

js经典原型与原型链的图
看上图,再引伸出一个问题:函数是对象。那你以为函数包含的大?仍是对象大呢? 如上图,对象是由函数创造的。 (Object created by Function) 也就是说,对象是 new Function获得的。 继续翻译,对象是实例 Function是构造函数。 继续翻译,对象这个实例有不可见属性 proto指向 Function构造函数的原型对象 (Function.prototype)。 故,函数与对象的关系是:函数更大,它包含对象。 这个我我的以为很重要,务必理解透。

3.2.4 一些概念的梳理

  • 全部函数都有一个特别的属性:
    • prototype : 显式原型属性
  • 全部实例对象都有一个特别的属性:
    • __proto__ : 隐式原型属性
  • 显式原型与隐式原型的关系
    • 函数的prototype: 定义函数时被自动赋值, 值默认为{}, 即用为原型对象
    • 实例对象的__proto__: 在建立实例对象时被自动添加, 并赋值为构造函数的prototype值
    • 原型对象即为当前实例对象的父对象
  • 原型链
    • 全部的实例对象都有__proto__属性, 它指向的就是原型对象
    • 这样经过__proto__属性就造成了一个链的结构---->原型链
    • 当查找对象内部的属性/方法时, js引擎自动沿着这个原型链查找
    • 当给对象属性赋值时不会使用原型链, 而只是在当前对象中进行操做

3.2.5 习题与案例

面试1:

/*阿里面试题*/function Person(){
    ②getAge = function (){
        console.log(10)
    }
    ③return this;
}

④Person.getAge = function (){
    console.log(20);
}

⑤Person.prototype.getAge = function (){
    console.log(30);
}

⑥var getAge = function (){
    console.log(40)
}

⑦function getAge(){
    console.log(50)
}


Q1:Person.getAge() // 20
Q2:getAge() // 40
Q3:Person().getAge() // 10
Q4:getAge() // 10
Q5:new Person().getAge() // 30
Q6:new Person.getAge(); // 20
复制代码

总体代码块①定义了构造函数Person ②是在构造函数中有一个未声明的变量,这个变量是引用变量,内容为地址值。指向一个函数对象。又由于,未使用严格模式下,在函数中不使用var声明的变狼都会成为全局变量。(注意这里不是属性,是全局变量)同时也要注意,这里②和③的语句在解析到这里后并无执行。执行的话就要看有没有new(做为构造函数使用),或者有没有加()调用(做为普通函数使用)。 ③返回一个this。这个this是谁如今还不知道。须要明白js中的this是动态的,因此根据上一节this的总结才定位到this究竟是谁。 ④Person.getAge是典型的“对象.属性(方法)”的形式,因此它是给Person函数对象上添加一个getAge的方法。等同于:

function Person.getAge(){
    console.log(20);
}
复制代码

函数名其实就是变量名。 ⑤在构造函数的原型中添加了getAge的方法 ⑥这里也是给一个全局变量赋值一个地址值,使其指向一个函数对象。注意,这里var的变量会声明提早。与代码块②区别,这里当解析完后,getAge已经指向一个函数对象啦。能够看作:

function getAge(){
    console.log(40)
}
复制代码

⑦定义一个函数,函数也会声明提早。在栈内存有getAge,内容值为一个地址值,指向一个函数对象。

Q1:对象.属性方法()。代码块④产生的结果。 Q2:调用函数,全局做用域里的。那只有代码块⑥产生结果。 Q3:Person().getAge()。先看前面一部分Person(),把Person当作一个普通函数调用,执行Person函数体对全局变量getAge进行定义并从新指向,也就是Person()执行了代码块②而覆盖了代码块⑥的操做。又返回this,根据Person()这种调用方式,可知this就是window。因此就是“window.gerAge()”,因被覆盖了,因此这行代码执行结果是代码块②产生。 Q4:getAge()至关于window.getAge(); 仍是上一个语句的结果,代码块②产生结果。 Q5:new Person()先看这部分,就是new出来一个实例,你能够想成p1,那么p1.getAge(); p1是一个Person的实例,p1中有不可见的[[proto]]属性,指向Person的原型对象。那么p1.getAge (),如今p1自己找,找不到就沿着原型链(proto指向链)去找,好找到了原型对向中有,由于代码块⑤产生做用。 Q6:new Person.getAge(); 能够把Person.getAge当作一个对象,去new它,是否是相似于咱们日常var p1=new Person();这样的操做,因此咱们把Person.getAge看作一个构造函数去new它。由上面对代码块④的理解,能够看作那样的函数,因此结果就是代码块④产生的结果。

面试题2:

function A () {

  }
  A.prototype.n = 1;
  var b = new A();
  A.prototype = {
    n: 2,
    m: 3
  };
  var c = new A();
  console.log(b.n, b.m, c.n, c.m);  //1 undefined 2 3
//见下图:
复制代码

js经典原型与原型链的图
面试题3:连续赋值问题

//与上题的区别在于如何理解a.x的执行顺序
<script>
var a = {n: 1};
var b = a;
a.x = a = {n: 2};  //先定义a.x再去从右往左赋值操做。
console.log(a.x);  // undefined  对象.属性 找不到 是返回undefined  变量找不到则报错!
console.log(b);  // {n :1, x : {n : 2}}
</script>
//见下图分析
复制代码

在内存结构

面试题4:

//构造函数F
  function F (){};
  Object.prototype.a = function(){
    console.log('a()')
  };
  Function.prototype.b = function(){
    console.log('b()')
  };
  //new一个实例对象f
  var f = new F();

  f.a();  //a()
  f.b();  //报错,找不到
  F.a();  //a()
  F.b();  //b()
复制代码

2.3 执行上下文与执行上下文栈

2.3.1 变量提高与函数提高

变量声明提高

  • 经过var定义(声明)的变量, 在定义语句以前就能够访问到
  • 值: undefined
  • 注:未使用var关键字声明变量时,该变量不会声明提高。
console.log(c); //报错,c is not defined.
    console.log(b); //undefined
    var b=0;
    c=4;
    console.log(c); //4 意外的全局变量->在ES5的严格模式下就会报错。
    console.log(b); //0 
复制代码

函数声明提高

  • 经过function声明的函数, 在以前就能够直接调用
  • 值: 函数定义(对象)
  • 注1:函数声明(Function Declaration)和函数表达式(Function Expression)是有微妙的区别,要明确他们两是Javascript两种类型的函数定义,两个概念上是并列的。也就是说定义函数的方式有两种:一种是函数声明,另外一种就是函数表达式。
  • 注2:函数表达式并不会声明提高。

先有变量提高, 再有函数提高

案例一:

var a = 3;
  function fn () {
    console.log(a);  //undefined
    var a = 4
  }
  fn();

//上面这段代码至关于
 var a = 3;
  function fn () {
      var a;
      console.log(a);  //undefined
      a = 4
  }
  fn();
复制代码

案例二:

console.log(b) //undefined 变量提高
  fn2() //可调用 函数提高
  fn3() //不能调用,会报错。 fn3是一个函数表达式,并不会函数提高,实际上他是变量提高。

  var b = 3
  function fn2() {
    console.log('fn2()')
  }
  var fn3 = function () {
    console.log('fn3()')
  }
复制代码

问题: 变量提高和函数提高是如何产生的? An:由于存在全局执行上下文和函数执行上下文的预处理过程。因此咱们就来学习下一节的执行上下文。

2.3.2 执行上下文

代码分类(位置)

  • 全局代码
  • 函数(局部)代码

执行上下文分为全局执行上下文和函数执行上下文

全局执行上下文

  • 步骤1:在执行全局代码前将window肯定为全局执行上下文对象(虚拟的)
  • 步骤2:对全局数据进行预处理(收集数据)
    • var定义的全局变量==>值为undefined, 并添加为window的属性
    • function声明的全局函数==>赋值(fun), 添加为window的方法
    • this==>赋值(window)
  • 步骤3:开始执行全局代码
//全局执行上下文
     console.log(a1);  //undefined
     console.log(a2);  //undefined
     a2();   //也会报错,a2不是一个函数
     console.log(a3);  //ƒ a3() {console.log('a3()')}
     console.log(a4)   //报错,a4没有定义
     console.log(this); //window

     var a1 = 3;
    //函数表达式,其实是变量提高。而不是函数提高。
     var a2 = function () {
       console.log('a2()')
     };
     function a3() {
       console.log('a3()')
     }
     a4 = 4;
复制代码

函数执行上下文

  • 步骤1:在调用函数, 准备执行函数体以前, 建立对应的函数执行上下文对象(虚拟的, 存在于栈中。栈会分为全局变量栈和局部变量栈,局部变量栈能够理解为是一个封闭的内存空间。虽然咱们编写的代码没法访问这个对象,但解析器在处理数据时会在后台使用它。)
  • 步骤2:对局部数据进行预处理(收集数据)
    • 形参变量==>赋值(实参)==>添加为执行上下文的属性
    • arguments==>赋值(实参列表), 添加为执行上下文的属性
    • var定义的局部变量==>undefined, 添加为执行上下文的属性
    • function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
    • this==>赋值(调用函数的对象)
  • 步骤3:开始执行函数体代码
//函数执行上下文
 function fn(a1) {
    console.log(a1);  //2 实参对形参赋值
    console.log(a2);  //undefined 函数内部局部变量声明提高
    a3();     //a3() 可调用 函数提高
    console.log(arguments);  //类数组[2,3]
    console.log(this);  //window

    var a2=3;
    function a3() {
      console.log("a3()");
    }
  }
  fn(2,3);  //执行,不执行不会产生函数执行上下文
复制代码

全局执行上下文和函数执行上下文的生命周期
全局 : 准备执行全局代码前产生, 当页面刷新/关闭页面时死亡
函数 : 调用函数时产生, 函数执行完时死亡

2.3.3 执行上下文栈

  • 执行上下文栈流程理解:
  1. 在全局代码执行前, JS引擎就会建立一个栈来存储管理全部的执行上下文对象
  2. 在全局执行上下文(window)肯定后, 将其添加到栈中(压栈)
  3. 在函数执行上下文建立后, 将其添加到栈中(压栈)
  4. 在当前函数执行完后,将栈顶的对象移除(出栈)
  5. 当全部的代码执行完后, 栈中只剩下window
<script type="text/javascript">
                            //1. 进入全局执行上下文
  var a = 10;
  var bar = function (x) {
    var b = 5;
    foo(x + b)              //3. 进入foo执行上下文
  };
  var foo = function (y) {
    var c = 5;
    console.log(a + c + y)
  };
  bar(10);   //2. 进入bar函数执行上下文(注:函数执行上下文对象在函数调用时产生,而不是函数声明时产生)
</script>
复制代码

以上这种状况整个过程产生了3个执行上下文 调用一次函数产生一个执行上下文 若是在上面代码最后一行的bar(10),再调用一次bar(10),那么就会产生5个上下文。 由于第一个bar(10)产生一个函数上下文 在bar函数中调用foo,又产生一个函数执行上下文。 那么如今又调用bar(10),与上面一个样会产生两个上下文,加起来4个函数执行上下文。 最后加上window的全局变量上下文,一共五个执行上下文。

递归
执行上下文
图注解1:这个过程有点像递归函数的回溯思想。在栈中,先有 window的全局上下文,而后执行 bar()会把 bar函数执行上下文压入栈中。 bar中调用 foo,把 foo函数执行上下文压入栈中, foo函数执行完毕,释放,便会把 foo函数执行上下文 pop(推出来)。逐渐 bar执行完毕, popbar函数执行上下文,最后只剩下 window上下文。

注解2:假设一个状况:f1()函数中会调用f2()f3()函数。那么在当前时刻栈中可最多达到几个上下文? An: 当f1()执行,会先调用f2(),调用完后,f2()已经完成了使命,它的生命周期就结束了,因此栈 就会释放掉他,在执行f3(),因此栈中也就最多三个上下文。f3() f1() window.

注解3:假设另外一个状况:f1()函数中会调用f2(), f2()中会调用f3()函数。那么在当前时刻栈中可最多达到几个上下文 ? An: 当f1()执行,会先调用f2(),执行f2()时要调用f3(),因此,栈中可达到4个上下文。f3() f2() f1() window .

2.3.4 习题与案例

面试题1:执行上下文栈

  1. 依次输出什么?
  2. 整个过程当中产生了几个执行上下文?
<script type="text/javascript">
  console.log('global begin: '+ i); //undefined 变量提高
  var i = 1;
  foo(1);
  function foo(i) {
    if (i == 4) {
      return;
    }
    console.log('foo() begin:' + i);
    foo(i + 1);
    console.log('foo() end:' + i);
  }
  console.log('global end: ' + i)  //1 全局变量i,其余的函数中的i当执行结束后就销毁了。
复制代码

执行结果:

&emsp;global begin: undefined
  foo() begin:1
  foo() begin:2
&emsp;foo() begin:3
&emsp;foo() end:3
&emsp;foo() end:2
&emsp;foo() end:1
&emsp;global end: 1
复制代码

一共产生5个上下文: 分析见下图,我画的很清楚了。这张图画了12min。主要就是入栈出栈,在出栈前,回溯原来的那个函数,那个函数执行上下文还在,若是还有 没有执行完的语句 会在这个时候执行。当剩余的语句已经执行完了,那么这个函数的执行上下文生命周期结束,释放出栈。想一想咱们递归调用去求阶乘的例子,思想是同样的。

递归思想

面试题2:变量提高和函数提高(执行上下文)

function fn(a){
    console.log(a);  // 输出function a的源码,a此时是函数a
    var a = 2;
    function a(){

    }
    console.log(a);  // 2
}
fn(1);
复制代码

考察点: 声明提早 难点: 函数优先

调用一开始, 就会先建立一个局部变量a, (由于a是形参), 而后把实参的值1赋值给aa= 1 几乎在同一时刻,那么一瞬间,开始处理函数内变量提高和函数提高 此时,a由于函数提高已经变成了a = function(){} 以上这些过程都是函数执行上下文的预处理过程 接下来,才是正式执行内部函数的代码。 console.log(a); 此时输出的就是function源码ƒ a(){} 结尾的输出语句便输出a = 2

测试题1: [考查知识点]先预处理变量, 后预处理函数

function a() {} //函数提高
  var a;  //变量提高
  //先预处理变量, 后预处理函数。也就是,函数提高会覆盖变量提高。
  console.log(typeof a); //function
复制代码

测试题2:[考查知识点] 变量预处理, in操做符 (在window上能不能找到b,无论有没有值)

if (!(b in window)) {
    var b = 1;  
    //在ES6以前没有块级做用域,因此这个变量b 至关于window的全局变量
  }
  console.log(b); //undefined
复制代码

测试题3: [考查知识点]预处理, 顺序执行 这个题笔者认为出的至关好。混乱读者的视角。固然再次强调,面试题是专门命题出来考查的,实际开发上可能有些不会这么用。但主要做用就是深刻理解。

var c = 1;
  function c(c) {
    console.log(c);
    var c = 3;
  }
  c(2); //报错。 c is not a function

  //这个题包含了变量和函数声明提高的问题,就是等价于如下的代码:
  var c;  //变量提高
  function c(c) {  //函数提高,覆盖变量提高
    console.log(c);
    var c = 3;   //函数内部的局部变量(在栈内存的封闭内存空间里,外面看不到)
  }
  c=1;//开始真正执行代码var c = 1
  console.log(c);
  c(2);  //c is not a function c是一个变量,值为number类型的数值.怎么能够执行?
复制代码

2.4 做用域与做用域链

2.4.1 做用域

1.理解:

  • 做用域:就是一块"地盘",一块代码区域, 在编码时就肯定了, 不会再变化(见下图解)定义函数变量时触发了做用域。执行结束完成做用域生命周期结束。
  • 做用域链:多个嵌套的做用域造成的由内向外的结构, 用于查找变量(见下图解)

2.分类:

  • 全局做用域
  • 函数做用域
  • js没有块做用域(但在ES6有了!)

3.做用

  • 做用域: 隔离变量, 能够在不一样做用域去定义同名的变量,不会形成冲突。不一样做用域下同名变量不会有冲突。例如,在全局中有一个变量b,那么在函数体中能不能有变量b,固然能够,这就是分隔变量。
  • 做用域链: 查找变量

给个案例:

var a = 10,
    b = 20
  function fn(x) {
    var a = 100,
      c = 300;
    console.log('fn()', a, b, c, x)
    function bar(x) {
      var a = 1000,
        d = 400
      console.log('bar()', a, b, c, d, x)
    }

    bar(100)
    bar(200)
  }
  fn(10);
复制代码

输出结果:

fn() 100 20 300 10
bar() 1000 20 300 400 100
bar() 1000 20 300 400 200
复制代码

4.做用域的图解以下:

做用域的图解

2.4.2 做用域与执行上下文

1.区别1

  • 全局做用域以外,每一个函数都会建立本身的做用域,做用域在函数定义时就已经肯定了。而不是在函数调用时。
  • 全局执行上下文是在全局做用域肯定以后, js代码立刻执行以前建立。
  • 函数执行上下文是在调用函数时, 函数体代码执行以前建立。

2.区别2

  • 做用域是静态的, 只要函数定义好了就一直存在, 且不会再变化。
  • 执行上下文是动态的, 调用函数时建立, 函数调用结束时就会自动释放(不是经过垃圾回收机制回收)。

3.联系

  • 执行上下文(对象)是从属于所在的做用域
  • 全局上下文环境==>全局做用域
  • 函数上下文环境==>对应的函数做用域

4.做用域与执行上下文图解以下:

做用域与执行上下文图解

2.4.3 做用域链

1.理解

  • 多个上下级关系的做用域造成的链, 它的方向是从下向上的(从内到外)
  • 查找变量时就是沿着做用域链来查找的

2.查找一个变量的查找规则

  • a.在当前做用域下的执行上下文中查找对应的属性, 若是有直接返回, 不然进入b
  • b.在上一级做用域的执行上下文中查找对应的属性, 若是有直接返回, 不然进入c
  • c.再次执行2的相同操做, 直到全局做用域, 若是还找不到就抛出找不到的异常

3.做用域链的图解以下:

做用域链的图解

2.4.4 习题与案例

面试题1:做用域

<script type="text/javascript">
  var x = 10;
  function fn() {
    console.log(x);  //10
  }
  function show(f) {
    var x = 20;
    f();
  }
  show(fn);
</script>
复制代码

记住: 做用域是代码一编写就肯定下来了,不会改变。产生多少个做用域?n+1. n就是多少个函数,1就是指的是window。查找变量就是沿着做用域查找,而做用域是一开始就肯定了,与哪里调用一点关系都没有。 见图解:

做用域
面试题:考察做用域与做用域链上的查找

<script type="text/javascript">
  var fn = function () {
    console.log(fn) //output: ƒ () {console.log(fn)}
  }
  fn()

  var obj = {
    fn2: function () {
     console.log(fn2) //报错,fn2 is not defined 
     console.log(this.fn2)//输出fn2这个函数对象  
    } 
  }
  obj.fn2()
</script>
复制代码

报错缘由:由于首先在这个匿名函数做用域找,找不到去上一层全局找,没找到,报错。找fn2是沿着做用域查找的! 输出fn2这个函数对象的缘由:若是要找到obj属性fn2,则用this.fn2(),让其用this这个指针指向obj,在obj这个对象中找fn2属性。

面试题3:考察连续赋值隐含的含义

(function(){
    var a = b = 3;
})();

console.log(a);  // 报错 a is not defined
console.log(b);  // 3
复制代码

理解:首先,赋值从右向左看。b = 3由于没var 因此至关于在全局做用域中添加b,赋值为3。如今。看前面var a=的部分,a有var,那么a就是局部变量放在栈内存的封闭内存空间上。var a=bb是变量,是基本数据类型的变量。它的内存中的内容值就是基本数据类型值,故拷贝一份给a。局部变量中a=3

匿名函数自执行,会有一个函数执行上下文对象。当函数执行完成,就会把执行上下文栈弹出这个上下文对象。就再也访问不到。

因此,在函数自执行结束后,再执行输出a的语句。a压根就找不到,根本没定义。b由于是全局变量仍是能够找到滴。

注意扩展,这里只有在非严格模式下,才会把b当作全局变量。若在ES5中严格模式下,则会报错。

面试4:考虑返回值问题

function foo1(){
    return {
        bar: "hello"
    }
}

function foo2(){
    return
    {
        bar: "hello"
    }
}

console.log(foo1()); // 返回一个对象 {bar:'hello'}
console.log(foo2());  // undefined
复制代码

解释:由于foo2函数return后面少了分号,在js引擎解析时,编译原理的知识可知,在词法分析会return后面默认加上分号,因此,后面那个对象压根不执行,压根不搭理。因此啊,当return时返回的就是undefined

面试5:函数表达式的做用域范围

<script>
console.log(!eval(function f() {})); //false
var y = 1;
if (function f(){}){
    y += typeof f;
}
console.log(y);  // 1undefined
</script>
复制代码

2.5 闭包

2.5.1 引入闭包

Code 1:

<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<script type="text/javascript">
  var btns = document.getElementsByTagName('button')
  //遍历加监听
  for (var i = 0,length=btns.length; i < length; i++) {
    var btn = btns[i]
    btn.onclick = function () {
      alert('第'+(i+1)+'个')
    }
  }
</script>
复制代码

输出结果:无论点击哪一个button,都是输出“第4个”。由于for循环一下就执行完了,但是btn.onclick是要等到用户事件触发的,故这个时候i3.永远输出“第4个”。 一些细节问题:在这个过程当中,产生了多少个i?一个i,i是全局变量啊。 事件模型的处理: 当事件被触发时,该事件就会对此交互进行响应,从而将一个新的做业(回调函数)添加到做业队列中的尾部,这就是js关于异步编程最基本的形式。 事件能够很好的工做于简单的交互,但将多个分离的异步调用串联在一块儿就会很麻烦,由于你必需要追踪到每一个事件的事件对象(例如上面的btn).此外你还要确保全部的事件处理程序都能在事件第一次触发以前被绑定完毕。例如,若btnonclick被绑定前点击,那么就不会有任何的事情发生。所以,虽然在响应用户交互或相似的低频功能时,事件颇有用,但它面对更复杂的需求时仍然不够灵活。 因此,从这个例子不只仅是对遍历加监听/闭包等理解。从这里也能够说明事件对象和事件机制的问题。因此,在ES6中会有promise和异步函数进行更多更复杂需求上的操做。

Code 2 经过对象.属性保存i

<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<script type="text/javascript">
  var btns = document.getElementsByTagName('button')
  //遍历加监听
  for (var i = 0,length=btns.length; i < length; i++) {
    var btn = btns[i]
    //将btn所对应的下标保存在btn上(解决方式1)
    btn.index = i
    btn.onclick = function () {
      alert('第'+(this.index+1)+'个')
    }
  }
</script>
复制代码

这个时候就是咱们想要的结果,点哪一个i ,button就输出第几个。

Code 3 经过ES6的块级做用域 let

<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<script type="text/javascript">
  var btns = document.getElementsByTagName('button')
  //遍历加监听
  for (let i = 0,length=btns.length; i < length; i++) {  //(解决方式二)
    var btn = btns[i]  
    btn.onclick = function () {
      alert('第'+(i+1)+'个')
    }
  }
</script>
复制代码

在ES6中引入块级做用域,使用let便可。

Code 4 利用闭包解决

<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<script type="text/javascript">
  var btns = document.getElementsByTagName('button')
  //利用闭包
  for (var i = 0,length=btns.length; i < length; i++) {  //这里的i是全局变量
    (function (j) {  //这里的j是局部变量
      var btn = btns[j]
      btn.onclick = function () { 
        alert('第'+(j+1)+'个')   //这里的j是局部变量
      }
    })(i); //这里的i是全局变量
  }
</script>
复制代码

for循环里有两个函数,btn.click这个匿名函数就是一个闭包。它访问了外部函数的变量。 产生几个闭包?3个闭包(外部函数执行几回就产生几个闭包)。每一个闭包都有变量j,分别保存着j=0,j=1,j=2的值。故能够实现这样的效果。以前之因此出问题,是由于都是用着全局变量的i,同一个i值。 闭包有没有被释放?没有,一直存在。咱们知道闭包释放,那就是让指向内部函数的引用变量为null便可。可是此时btn.onclick一直引用这内部函数(匿名函数),故其闭包不会被释放。 闭包应不该该被释放?不该该。由于一个页面的一个button是要一直存在的,页面显示过程当中,button要一直关联着这个闭包。才能让每点击button1alert(第1个)这样的结果。不可能让button点击了一次就失效吧。那么假设要释放这些闭包,那就让btn.onclick=null便可。 闭包的做用?延长局部变量j的生命周期。

2.5.2 理解闭包(Closure)

1.如何产生闭包?

  • 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时, 就产生了闭包。
  • 注1:若外部函数有变量b,而内部函数中没有引用b,则不会产生闭包。

2.闭包究竟是什么?
闭包是指有权访问另外一个函数做用域中的变量的函数。
能够理解为:
包含了那个局部变量的容器(不必定是对象,相似对象)
他被内部函数对象引用着
怎么判断闭包存在否?最终就是判断函数对象有没有被垃圾回收机制。

  • 使chrome`能够调试查看闭包的存在
  • 理解一: 闭包是嵌套的内部函数(绝大部分人)
  • 理解二: 包含被引用变量(函数)的对象(极少数人)
  • 若理解二请注意: 闭包存在于嵌套的内部函数中。
  • 口语:首先明白闭包的本质上是一个对象,保存在内部函数中的对象,这个对象保存着被引用的变量。
  • 在后台执行环境中,闭包的做用域包含着它本身的做用域、包含函数的做用域和全局做用域。

3.产生闭包的条件?

  • 函数嵌套
  • 内部函数引用了外部函数的数据(变量/函数)
  • 执行外部函数(内部函数能够不执行)

案例1:

function fn1 () {
      var a = 2
      var b = 'abc'
      function fn2 () { //执行函数定义就会产生闭包(不用调用内部函数)
        console.log(a)  //引用了外部函数变量,若里面没有引用任何的外部函数变量(函数)则不会产生闭包
      }
      // fn2() 内部函数能够不执行,也会产生闭包。只要执行了内部函数的定义就行。但如果函数表达式呢?
    }
    fn1(); //外部函数要执行哦,不然不会产生闭包
复制代码

案例2:

function fun1() {
      var a = 3
      var fun2 = function () {
        console.log(a)
      }
    }
    fun1()
  //这样子经过函数表达式定义函数,若没有在里面调用内部函数,则不会产生闭包。
复制代码

案例3:

function fun1() {
      var a = 3
      var fun2 = function () {
        console.log(a)
      }
      fun2()
    }
    fun1()
  //这样子经过函数表达式定义函数,但在里面调用了内部函数,那么这个状况是能够产生闭包的。
复制代码

函数表达式不一样于函数声明。函数声明要求有名字,但函数表达式不须要。没有名字的函数表达式也叫作匿名函数(anonymous function),匿名函数有时候也叫拉姆达函数,匿名函数的 ·name· 属性是空字符串。

以上这些案例,只是辅助理解。并无实际上的应用,下面就来讲说闭包实际能够应用的地方。

2.5.3 常见的闭包

  1. 将函数做为另外一个函数的返回值
  2. 将函数做为实参传递给另外一个函数调用

案例1:将函数做为另外一个函数的返回值

function fn1() {
    var a = 2
    function fn2() {
      a++
      console.log(a)
    }
    return fn2
  }
  var f = fn1()
  f() // 3
  f() // 4
复制代码

深刻理解: 问题一:有没有产生闭包? An:条件一,函数的嵌套。外部函数fn1,内部函数fn2,条件一知足;条件二,内部函数引用了外部函数的数据(变量/函数)。a就是外部函数的数据变量。条件二知足。条件三,执行外部函数。var f = fn1()其中的fn1()是否是执行了,外部函数执行了。赋值给f变量是由于外部函数fn1在执行后返回一个函数,用全局变量f来保存其地址值。条件三知足。综上所述,产生了闭包。

问题二:产生了几个闭包? 产生了一个闭包。咱们根据上一节的知识可知:执行函数定义就会产生闭包。那么执行函数定义是否是只要执行外部函数便可,由于外部函数一执行,就会有函数上下文对象,就会函数声明提早,也就是执行了函数定义。那么,这个时候执行了几回外部函数?是否是一次。执行了一次外部函数,也就是声明函数提早了一次,执行函数定义这个操做作了一次,故只产生了一个闭包。也可得出结论,外部函数执行几回,就产生几个闭包。跟内部函数执行几回没有关系(前提,在能够生成闭包的状况下)

问题三:调用内部函数为何能够读出a的最新值? 从结果能够知道,f() ,f()是否是在调用了两次内部函数,从输出的结果看,a每次输出最新值。这就能够知道,在执行内部函数的时候,a并无消失。记住这点,这就是闭包的本质做用。

问题四:那么若是我如今在以上代码最后(分别输出3,4语句后面)继续加入

var h =fn1();
  h(); 
  f(); 
复制代码

这个时候会输出什么? An:h()会输出3. f()会输出5. 由于:var h = fn1()又执行了一次,h接收返回值函数对象(内部函数),也就是在这个时候产生了新的一个闭包。当调用内部函数时,h(),就会有新的函数上下文对象产生,a值就会从初始值开始记录。当调用f()时,这个时候仍是在上一个闭包的状态下,那个做用域并无消失,故还在原先的基础上改变a值。

案例2. 将函数的实参传递给另外一个函数调用(★★★)

function showDelay(msg, time) {
    setTimeout(function () {
      alert(msg)
    }, time)
  }
  showDelay('my name is ly', 2000)
复制代码

这个例子说明了,咱们要使用闭包不必定要return出去。只要这个函数对象被引用着就行。return的话那我再外面用变量接收一下就引用着了。可是我使用定时器,定时器模块式在浏览器分线程运行着的,定时器这个回调函数就是定时器模块保存管理着。

深刻理解: 问题一:有没有产生闭包? An:条件一,函数的嵌套。外部函数showDelay ,内部函数定时器的回调函数,条件一知足;条件二,内部函数引用了外部函数的数据(变量/函数)。msg就是外部函数的数据变量,而在回调函数中用了。注意,time不是哦,time仍是在外部函数用的,在内部函数中并无用到外部函数的time变量。是由于msg变量才知足条件二。条件三,执行外部函数。showDelay('my name is ly', 2000)执行了,外部函数执行了。可是注意回调函数没有声明提高,故还要等2000ms后触发进行调用回调函数。这个时候内部函数才执行。条件三知足(这个相似于函数表达式状况,若是是函数表达式,那么不只仅要外部函数要执行,内部函数表达式定义的函数也要有执行,只有这样才会出现闭包。若是是函数声明定义的函数,那么就会有函数执行上下文去建立,函数提高,故在执行函数定义的时候就会出现闭包)。综上所述,产生了闭包。

2.5.4 闭包的做用

1.使用函数内部的局部变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期) 原本,局部变量的生命周期是否是函数开始执行到执行完毕,局部变量就自动销毁啦。可是,经过闭包能够延长局部变量的生命周期,函数内部的局部变量能够在函数执行完成后继续存活在内存中。那就是经过闭包,具体怎样的内部机制见下。

2.让函数外部能够操做(读写)到函数内部的数据(变量/函数) 原本,函数内部是能够经过做用域链去由内向外去找数据(变量/函数),是能够访问到函数外部的。可是反过来是不行的,函数外部能访问到内部的数据(变量/函数)吗?

function fun1() {
    var a='hello world'
  }
  console.log(a); //报错 a is not defined 
  //函数外部不能访问函数内部的数据
复制代码

因此,函数外部不能访问函数内部的数据。可是,能够经过闭包去访问到函数内部的数据。具体的内部机制又是怎样的呢?见下。

//详解闭包的做用(重要)
function fn1() {
    var a = 2
    function fn2() {
      a++
      console.log(a)
    }
    function fn3() {
      a--
      console.log(a)
    }
    return fn3
  }
  
  var f = fn1()
  f() // 1
  f() // 0
复制代码

问题1. 函数fn1()执行完后, 函数内部声明的局部变量是否还存在? An: 通常是不存在, 存在于闭包中的变量才可能存在。像fn2 fn3变量就自动销毁了。由于函数内的局部变量的生命周期就是函数开始执行到执行完毕的过程。那像fn2这个函数对象也会被垃圾回收机制回收,由于没有变量去引用(指向)fn2函数对象。可是fn3这个对象还在,根本缘由是由于语句 var f = fn1(); fn( )执行完毕返回一个fn3的地址值而且赋值给全局变量f,那么全局变量f就会指向他,因此,这个fn3这个函数对象不会被垃圾回收机制回收。 但我在回答这个问题时,是说存在于闭包中的变量才可能存在。为何可能呢?由于若是我把语句var f = fn1(); 改为fn1(),这个时候仍是没有变量去引用,因此这时仍是会被回收的。见下图。

在这里插入图片描述
那么咱们发现,闭包一直会存在吗?这就是咱们下一节要讲的闭包的生命周期。提早说一下,也就是 fn3函数对象一直会有引用,闭包就会存在。这时我只要将 f=null,这个时候 fn3函数对象就没有被 f引用,因此会被垃圾回收机制回收,故此时这个闭包死亡。

问题2:在函数外部能直接访问函数内部的局部变量吗? An: 不能, 但咱们能够经过闭包让外部操做它.

2.5.5 闭包的生命周期

  1. 产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)--->针对的是用函数声明的内部嵌套函数。
  2. 死亡: 在嵌套的内部函数成为垃圾对象时。
function fn1() {
    //此时闭包就已经产生了(函数提高, 内部函数对象已经建立了)
    var a = 2
    function fn2 () {
      a++
      console.log(a)
    }
    return fn2
  }
  var f = fn1()
  f() // 3
  f() // 4
  f = null //闭包死亡(包含闭包的函数对象成为垃圾对象)
复制代码

2.5.6 闭包的应用

闭包应用:

  • 模块化: 封装一些数据以及操做数据的函数, 向外暴露一些行为。(本节具体讲)从这能够引出四大模块化思想。
  • 循环遍历加监听(在2.5.1引入闭包章节有讲)
  • JS框架(jQuery)大量使用了闭包

闭包的应用之一:定义JS模块

  1. 要具备特定功能的js文件

  2. 将全部的数据和功能都封装在一个函数内部(私有的)(函数内部会有做用域与做用域链的概念,函数内部的数据就是私有的,外部访问不到。)

  3. 只向外暴露一个包含n个方法的对象(暴露多个行为)或函数(暴露一个行为)

  4. 模块的使用者, 只须要经过模块暴露的对象调用方法来实现对应的功能

自定义JS模块一:

function myModule() {
  //私有数据
  var msg = 'Hello world';
  //操做数据的函数
  function doSomething() {
    console.log('doSomething() '+msg.toUpperCase())
  }
  function doOtherthing () {
    console.log('doOtherthing() '+msg.toLowerCase())
  }

  //向外暴露对象(给外部使用的方法)
  return {
    doSomething: doSomething,
    doOtherthing: doOtherthing
  }
}

//怎么使用?在html页面中
  var module = myModule()
  module.doSomething()
  module.doOtherthing()
复制代码

自定义JS模块二:

(function () {
  //私有数据
  var msg = 'My atguigu'
  //操做数据的函数
  function doSomething() {
    console.log('doSomething() '+msg.toUpperCase())
  }
  function doOtherthing () {
    console.log('doOtherthing() '+msg.toLowerCase())
  }

  //向外暴露对象(给外部使用的方法)
  window.myModule2 = {
    doSomething: doSomething,
    doOtherthing: doOtherthing
  }
})();

//怎么使用?在html页面中
  myModule2.doSomething()
  myModule2.doOtherthing()
//这种自定义模块相对而言更好,由于不须要先调用外部函数,直接使用 myModule2.doSomething()更加方便。
复制代码

2.5.7 闭包的缺点和解决

1.缺点

  • 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长(是优势亦是缺点)
  • 容易形成内存泄露(注意和内存溢出的区别,见1.5.3节)

2.解决

  • 能不用闭包就不用
  • 及时释放
<script type="text/javascript">
  function fn1() {
    var arr = new Array[100000]
    function fn2() {
      console.log(arr.length)
    }
    return fn2
  }
  var f = fn1()
  f()
  //这里是有闭包的,arr一直没有释放,很占内存。
  //如何解决?很简单。
  f = null //让内部函数成为垃圾对象-->回收闭包

</script>
复制代码
  • 理解:

    • 当嵌套的内部函数引用了外部函数的变量时就产生了闭包
    • 经过chrome工具得知: 闭包本质是内部函数中的一个对象, 这个对象中包含引用的变量属性
  • 做用:

    • 延长局部变量的生命周期
    • 让函数外部能操做内部的局部变量
  • 写一个闭包程序

function fn1() {
    var a = 2;
    function fn2() {
      a++;
      console.log(a);
    }
    return fn2;
  }
  var f = fn1();
  f();
  f();
复制代码
  • 闭包应用:

    • 模块化: 封装一些数据以及操做数据的函数, 向外暴露一些行为
    • 循环遍历加监听
    • JS框架(jQuery)大量使用了闭包
  • 缺点:

    • 变量占用内存的时间可能会过长
    • 可能致使内存泄露
    • 解决:
      • 及时释放 :f = null; //让内部函数对象成为垃圾对象

2.6.9 习题与案例

面试1:考察闭包

function foo(){
    var m = 1;
    return function (){
        m++;
        return m;
    }
}

var f = foo();  //这会造成一个闭包 (调用一次外部函数)
var f1 = foo();  //这会造成一个闭包 (调用一次外部函数)
/*不一样闭包有不一样做用域。同一个闭包能够访问其最新的值。--这句话知识一个表面现象,结合上面的案例去发现深刻的步骤,这个过程是如何执行的?*/
console.log(f()); // 2
console.log(f1()); // 2
console.log(f()); // 3
console.log(f()); // 4
复制代码

面试2:闭包相关知识

<script>
function fun(n, o){
    console.log(o);  //实则就是输出闭包中的变量值,n是闭包引用的变量。延长的是n的变量生命周期。
    return {
        fun: function (m){
            return fun(m, n);
        }
    }
}
/*注意以上代码段是有闭包的,return fun(m,n)中的n是用到了外部函数的变量n*/

//测试一:
var a = fun(0);  // undefined
a.fun(1); // 0  执行这里实则是产生了新的闭包,但没有变量去指向这个内部函数产生的闭包故立刻就消失啦。
a.fun(2); // 0
a.fun(3); // 0
/*最后三行语句一直用的闭包是fun(0)产生的闭包*/

//测试二: 
var b = fun(0).fun(1).fun(2).fun(3); //undefined 0 1 2
/*产生了四个闭包,也就是外部函数fun(n,o)调用过4次。*/

// 测试三:
var c = fun(0).fun(1); // undefined 0
c.fun(2)  // 1
c.fun(3)  // 1
/*最后两行语句一直用的闭包是fun(0).fun(1)产生的闭包,故其语句*/

</script>
复制代码

面试3:写一个函数, 使下面的两种调用方式都正确

console.log(sum(2,3));   // Outputs 5
console.log(sum(2)(3));  // Outputs 5
复制代码

答案:

<script>
function sum(){
    if(arguments.length == 2){
        return arguments[0] + arguments[1];
    }else if(arguments.length == 1){
        var first = arguments[0];
        return function (a){
            return first + a;
        }
    }
}
</script>
复制代码

此文档为吕涯原创,可任意转载,但请保留原连接,标明出处。 文章只在CSDN和掘金第一时间发布: CSDN主页:https://blog.csdn.net/LY_code 掘金主页:https://juejin.im/user/5b220d93e51d4558e03cb948 如有错误,及时提出,一块儿学习,共同进步。谢谢。 😝😝😝