JS的函数篇(4.3W字)

3,815 阅读50分钟

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

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

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

3.1 this的使用总结

this是在函数执行的过程中自动创建的一个指向一个对象的内部指针。确切的说,this并不是一个对象,而是指向一个已经存在的对象的指针,也可以认为是this就是存储了某个对象的地址。

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

3.1.1 搞懂this指向的预备知识

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

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

    理解完以上的这句话,我们在来说明一下其中的区别,这是很多人没有关注过的。

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

    注解:也就是在未对变量(a)进行声明时,就会出现以上结果。首先明确一点,就是全局变量a window的属性。所以,我们从这里就可以发现,什么时候undefined,什么时候报错?——那就是如果是访问一个对象的属性时,它没有声明赋值,那就是undefined;如果访问一个变量,它没有声明赋值,那就是报错。

    好,现在回头过来,我们看全局作用域变量和函数的认识。

    <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 规范给所有函数都定义了 call apply 两个方法,它们的应用非常广泛,它们的作用也是一模一样,只是传参的形式有区别而已。

简单来说,假设有一个函数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)
}


Q1Person.getAge() // 20
Q2getAge() // 40
Q3Person().getAge() // 10
Q4getAge() // 10
Q5new Person().getAge() // 30
Q6new 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当执行结束后就销毁了。

执行结果:

 global begin: undefined
  foo() begin:1
  foo() begin:2
 foo() begin:3
 foo() end:3
 foo() end:2
 foo() end:1
 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赋值给a a= 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主页:blog.csdn.net/LY_code
掘金主页:juejin.im/user/366762…
若有错误,及时提出,一起学习,共同进步。谢谢。 😝😝😝