美文网首页前端知识
JavaScript中apply/call/bind和this详

JavaScript中apply/call/bind和this详

作者: nucky_lee | 来源:发表于2019-10-11 15:42 被阅读0次

    相关知识点:

    1. 作用域

    作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。

    局部作用域
    function outer(){ 
      // 声明变量(局部变量)  
      var name = "ukerxi"; 
      // 定义内部函数  
      function inner() { 
        console.log(name); // 可以访问到 name 变量
       } 
    }
    console.log(name); // 报错,undefined;
    

    name是函数内部声明并赋值,拥有局部作用域,只能在函数outer内部使用,在outer外部使用就会报错,这就是局部作用域的特性,外部无法访问。

    全局作用域

    任何地方都能访问到的对象拥有全局作用域。

    (1)函数外面定义的变量拥有全局作用域

    (2)未定义直接赋值的变量自动声明为拥有全局作用域

    function  outFun2() { 
      variable = "未定义直接赋值的变量"; 
      var inVariable2 = "内层变量2"; 
    } 
    outFun2(); // 要先执行这个函数,否则根本不知道里面是啥  
    console.log(variable);  // 未定义直接赋值的变量  
    console.log(inVariable2);  // inVariable2 is not defined
    

    (3)window对象的属性拥有全局作用

    块级作用域

    块级作用域可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:

    1. 在一个函数内部
    2. 在一个代码块(由一对花括号包裹)内部

    let 声明的语法与 var 的语法一致。你基本上可以用 let 来代替 var 进行变量声明,但会将变量的作用域限制在当前代码块中。

    {
      let a = 10;
      var b = 1;
    }
    
    a // ReferenceError: a is not defined.
    b // 1
    

    块级作用域有以下几个特点:

    • 声明变量不会提升到代码块顶部
      let/const 声明并不会被提升到当前代码块的顶部,因此你需要手动将 let/const 声明放置到顶部,以便让变量在整个代码块内部可用。
    function  getValue(condition) {
      if (condition) { 
        let value = "blue"; 
        return value; 
      } else { 
        // value 在此处不可用  
        return  null; 
      }
      // value 在此处不可用 
    }
    
    • 同一作用域内禁止重复声明
      如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行 let 声明就会导致抛出错误。例如:
    var count = 30; 
    let count = 40; // Uncaught SyntaxError: Identifier 'count' has already been declared
    

    在本例中, count 变量被声明了两次:一次使用 var ,另一次使用 let 。因为 let 不能在同一作用域内重复声明一个已有标识符,此处的 let 声明就会抛出错误。

    • 循环中的绑定块作用域

    for循环的计数器,就很合适使用let命令。

    for (let i = 0; i < 10; i++) {
      // ...
    }
    
    console.log(i); // ReferenceError: i is not defined
    

    上面代码中,计数器i只在for循环体内有效,在循环体外引用就会报错。

    下面的代码如果使用var,最后输出的是10。

    var a = [];
    for (var i = 0; i < 10; i++) {
      a[i] = function () {
        console.log(i);
      };
    }
    
    a[6](); // 10
    

    上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。

    如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。

    var a = [];
    for (let i = 0; i < 10; i++) {
      a[i] = function () {
        console.log(i);
      };
    }
    
    a[6](); // 6
    

    上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

    另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

    for (let i = 0; i < 3; i++) {
      let i = 'abc';
      console.log(i);
    }
    
    // abc
    // abc
    // abc
    

    上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。

    作用域链

    1.什么是自由变量

    当前作用域没有定义的变量,为自由变量 。自由变量的值如何得到 —— 要到创建 fn 函数的那个作用域中取,无论 fn 函数将在哪里调用。

    1. 什么是作用域链

    通俗地讲,当声明一个函数时,局部作用域一级一级向上包起来,就是作用域链。

    var x = 10
    function fn() {
        console.log(x) //x为自由变量
    }
    
    function show(f) {
      var x = 20;
      (function() {
          f() //10,而不是20
      })()
    }
    
    show(fn) //10
    

    闭包

    闭包就是能够读取其他函数内部变量的函数。

    优点:闭包可以形成独立的空间,永久的保存局部变量。

    缺点:闭包中的局部变量永远不会被回收,容易造成内存泄漏。

    如何从外部读取局部变量?

    出于种种原因,我们有时候需要得到函数内的局部变量。但是,正常情况下这是办不到的,只有通过变通方法才能实现。
    那就是在函数的内部,再定义一个函数。

    Js代码

    function f1(){
      n=999;
      function f2(){
        alert(n); // 999
      }
    }
    

    在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1 是不可见的。这就是Javascript语言特有的“链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

    既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

    Js代码

    function f1(){
     n=999;
     function f2(){
      alert(n);
     }
     return f2;
    }
    
    var result=f1();
    
    result(); // 999
    
    闭包的用途

    闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

    怎么来理解这句话呢?请看下面的代码。

    Js代码

    function f1() {
     var n = 999;
    
     nAdd = function(){n+=1};
    
     function f2(){
      alert(n);
     }
    
     return f2;
    }
    
    var result=f1();
    result(); // 999
    nAdd();
    result(); // 1000
    

    在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

    为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

    这段代码中另一个值得注意的地方,就是“nAdd=function(){n+=1}”这一行,首先在nAdd前面没有使用var关键字,因此 nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数,而这个
    匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

    使用闭包的注意点

    1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

    2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

    思考题

    如果你能理解下面代码的运行结果,应该就算理解闭包的运行机制了。

    var name = "The Window";   
    
    var object = {   
     name : "My Object",   
     getNameFunc : function(){   
      return function(){   
       return this.name;   
            };   
     }   
    };   
    
    alert(object.getNameFunc()());  //The Window
    

    我们可以分解一下:

    var func = object.getNameFunc(); //可以认为 func 内部的name为自由变量,func是在全局定义的,因此name应该在全局作用域查找。
    
    func() //The Window 此时this指向window
    

    2. this 指向

    this对象是在函数运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被当作某个对象的方法调用时,this等于那个对象。

    判断方法:this和定义在哪儿无关,函数运行时,如果有. 运算符,this指.前的对象;如果没有,this指window。若new关键字调用时,即构造函数体内部使用了this关键字,代表了所要生成的对象实例。有apply/call/bind时,指代第一个参数。

    /例1/

    function  foo() {
      console.log( this.a );
    }
    
    var  obj2 = {
      a: 42,
      foo: foo
    };
    
    var  obj1 = {
      a: 2,
      obj2: obj2
    };
    
    obj1.obj2.foo(); // 42; 当foo函数被调用时,其本身是归obj2所拥有
    

    /例2/

    function  foo() {
      console.log( this.a ); //自由变量
    }
    
    var  obj = {
      a: 2,
      foo: foo
    };
    
    var  bar = obj.foo;
    var  a = "global"; // 全局对象的属性
    bar(); // "global" ; 
    
    1. 函数柯里化;

    https://www.jianshu.com/p/2975c25e4d71

    柯里化,Currying,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

    看这个解释有一点抽象,我们就拿被做了无数次示例的add函数,来做一个简单的实现。

    // 普通的add函数
    function add(x, y) {
      return x + y
    }
    
    // Currying后
    function curryingAdd(x) {
      return function (y) {
        return x + y
      }
    }
    
    add(1, 2) // 3
    
    curryingAdd(1)(2) // 3
    

    实际上就是把add函数的x,y两个参数变成了先用一个函数接收x, 然后返回一个函数去处理y参数。现在思路应该就比较清晰了,就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

    函数的length属性返回函数预期传入的参数个数

    4. 原型与原型链;

    https://wangdoc.com/javascript/oop/prototype.html

    call/apply/bind 的联系与区别

    三者都可用于显示绑定 this;

    call/apply 的区别方式在于参数传递方式的不同;

    fn.call(obj, arg1, arg2, ...)
    
    fn.apply(obj, [arg1, arg2, ...])
    

    call()方法接受的是若干个参数的列表,而apply()方法接受的是一个包含多个参数的数组。

    bind 返回的是一个待执行函数,是函数柯里化的应用,而 call/apply 则是立即执行函数

    call 的源码实现

    Function.prototype.myCall = function(context) {
    
      if (typeof context === 'object') {
        context = context || window;
      } else {
        context = Object.create(null);//空对象,不会受到原型链的干扰。原型链终端指向 null,不会有构造函数,也不会有 toString、 hasOwnProperty、valueOf 等属性
      }
    
      console.log('myCall context->', context); //{name: "aaaaa"}
    
      console.log('myCall this->', this);
      // ƒ (msg) {
      // console.log('我的名字' + this.name + msg);
      // }
    
      //用Symbol来做属性 key 值,保持唯一性,避免冲突
      let fn = Symbol();
    
      // 在传入的上下文对象中,创建一个属性,值指向方法 sayHi,此时方法中的作用域已经改变context
      context[fn] = this;
    
      //接收参数,排除第一个参数this
      let args = [...arguments].slice(1);
    
      const result = context[fn](args);
    
      //删除避免永久存在
      delete(context[fn]);
    
      return result;
    }
    

    验证一下:

    var mine = { name:'aaaaa' };
    
    var person = {
        name: 'bbbb',
        sayHi: function(msg) {
        console.log('我的名字' + this.name + msg);
      }
    }
    
    console.log(person.sayHi.myCall(mine, '很高兴见到你!')); //我的名字aaaaa很高兴见到你!
    
    应用场景:

    1、将类数组转化为数组
    Array.prototype.slice.call(arguments);

    apply 的源码实现

    Function.prototype.myApply = function(context) {
    
      if (typeof context === 'object') {
        context = context || window;
      } else {
        context = Object.create(null);
      }
    
      let fn = Symbol();
    
      //为上下文添加属性, 值为方法this
      context[fn] = this;
    
      let result;
      if (arguments[1]) {
        //如果有参数数组,参入this方法
        result = context[fn](...arguments[1]);
      } else {
        result = context[fn]();
      }
    
      delete(context[fn]);
    
      return result;
    }
    
    应用场景:

    1、求数组中的最大和最小值

    Math.max()    //只接收单独的参数,通过下面的方法可以在数组上面使用max方法:
    Math.max.apply(null, array);    //会将array数组参数展开成单独的参数再传入
    
    var arr = [1,2,3,89,46]
    var max = Math.max.apply(null,arr)//89
    var min = Math.min.apply(null,arr)//1
    

    1、数组追加
    Array.prototype.push.apply(arr1,arr2); //将一个数组拆开push到另一个数组中;不用apply则会将后续数组参数当成一个元素push进去。

    var arr1 = [1,2,3];
    var arr2 = [4,5,6];
    var total = [].push.apply(arr1, arr2);//6
    // arr1 [1, 2, 3, 4, 5, 6]
    // arr2 [4,5,6]
    

    Function.prototype.bind()

    bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()中的第一个参数的值,例如:f.bind(obj),实际上可以理解为obj.f(),这时f函数体内的this自然指向的是obj;

    function f(y,z){
      return this.x+y+z;
    }
    
    var m = f.bind({x:1},2);
    console.log(m(3)); // 6
    

    分析:
    // 这里的bind方法会把它的第一个实参绑定给f函数体内的this,所以f函数里的this即指向{x:1}对象;
    // 从第二个参数起,会依次传递给原始函数,这里的第二个参数2即是f函数的y参数;
    // 最后调用m(3)的时候,这里的3便是最后一个参数z了,所以执行结果为1+2+3=6;
    // 分步处理参数的过程其实是一个典型的函数柯里化的过程(Curry)。

    使用bind方法一
    var a = {
    
      b: function() {
        var func = function() {
        console.log(this.c);
        }.bind(this);
        func();
      },
    
      c: 'hello'
    }
    
    a.b(); // hello
    console.log(a.c); // hello
    
    使用bind方法二
    var a = {
      b: function() {
        var func = function() {
          console.log(this.c);
        }
        func.bind(this)();
      },
    
      c: 'hello'
    }
    
    a.b(); // hello
    console.log(a.c); // hello
    

    bind 的源码实现

    Function.prototype.myBind = function(context) {
      // bind 调用的方法一定要是一个函数
      if (typeof this !== 'function') {
        throw new TypeError('not a function');
      }
      
      var args = Array.prototype.slice.call(arguments, 1);
    
      // 记住当前作用域,指向调用者。
      let self = this;
    
      let bound = function() {
        // 将前后参数合并传入
        var innerArgs = Array.prototype.slice.call(arguments);
        var finalArgs = args.concat(innerArgs);
        // 当作为构造函数时,this 指向实例,此时 this instanceof fBound 结果为 true,可以让实例获得来自绑定函数的值
        return self.apply(this instanceof bound ? this : context || this, finalArgs);
      }
    
      // 还要考虑修改返回函数的prototype为绑定函数的prototype,使得实例可以继承原型的值。 
      // 为了修改 bound.prototype时不影响原型的值,使用ES5的 Object.create()方法创建一个空对象,继承this的 prototype 属性
      bound.prototype = Object.create(this.prototype)
    
      return bound;
    }
    

    // 测试用例

    var value = 2;
    var foo = {
        value: 1
    };
    function bar(name, age) {
        this.habit = 'shopping';
        console.log(this.value);
        console.log(name);
        console.log(age);
    }
    bar.prototype.friend = 'kevin';
     
    var bindFoo = bar.bind2(foo, 'Jack'); // bindFoo 为返回的bound函数
    
    //使用new操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略(this 指向实例obj),同时调用时的参数被提供给模拟函数。
    var obj = new bindFoo(20); // 返回正确
    // undefined
    // Jack
    // 20
     
    obj.habit; // 返回正确
    // shopping
     
    obj.friend; // 返回正确
    // kevin
     
    obj.__proto__.friend = "Kitty"; // 修改原型
     
    bar.prototype.friend; // Kevin
    

    相关文章

      网友评论

        本文标题:JavaScript中apply/call/bind和this详

        本文链接:https://www.haomeiwen.com/subject/fwztmctx.html