# 实现call/apply/bind

考点:

  • call/apply/bind的功能
  • js中this的指向
  • 原型链

# js中this的指向

1.当函数作为构造函数, 通过 new xxx() 调用时, this指向生成的实例

    function cat(name, color) {
      this.name = name
      this.color = color
    }
    let cat1 = new cat('大毛','橘色') //this指向cat1
    let cat2 = new cat('二毛','黑色') //this指向cat2
    console.log(cat1)
    console.log(cat2)
1
2
3
4
5
6
7
8

2.当函数直接被调用时 (通过 xxx()的方式调用) this指向window对象, 严格模式下为 undefined

    function Dog(name, color) {
      this.name = name
      this.color = color 
    }
    Dog('大毛','橘色')
    console.log(window.name)  // 大毛
    console.log(window.color) // 橘色
1
2
3
4
5
6
7

3.当函数被调用时, 它是作为某个对象的方法 (前面加了点 '.') this指向这个对象 (点 '.' 前面的对象) (谁调用它, this就指向谁)

    function setDetails(name, color) {
      this.name = name
      this.color = color
    }
    let pig = {}
    pig.setDetails = setDetails
    pig.setDetails('大毛','红色')
    console.log(pig.name)  // 大毛
    console.log(pig.color) // 红色
1
2
3
4
5
6
7
8
9

思考1

    let obj = {
      x:10,
      fn: function() {
        function a() {
          console.log(this.x)  
          console.log(this) // this 指向 window
        }
        a()
      }
    }
    obj.fn()  // 输出什么?  undefined
1
2
3
4
5
6
7
8
9
10
11

思考2

    let obj2 = {
      x: 10,
      fn:function() {
        console.log(this.x)
      }
    }
    let a = obj2.fn
    obj2.fn()  // 输出什么?  10
    a()  // 输出什么?  undefined
1
2
3
4
5
6
7
8
9

思考3

    let obj3 = {
      x:10,
      fn: function() {
        return function() {
          console.log(this.x)
        }
      }
    }
    obj3.fn()()  // 输出什么  undefined
1
2
3
4
5
6
7
8
9

# 实现call

函数通过call调用时, 函数体内的this指向call方法传入的第一个实参, 而call方法后续的实参会依次传入作为原函数的实参传入

    function setDetails(name, color) {
      this.name = name
      this.color = color
    }
    let cat1 = {}
    let cat2 = {}
    setDetails.call(cat1, '大毛','橘色')
    setDetails.call(cat2, '二毛','黑色')
    console.log(cat1.name) // 大毛
    console.log(cat2.name) // 二毛
1
2
3
4
5
6
7
8
9
10
    let person1 = {
      name: 'zs',
      say: function(hobby) {
        console.log(this.name)
        console.log('爱好:' + hobby)
      }
    }
    let person2 = {
      name: 'ls'
    }
    person1.say('打游戏')
    person1.say.call(person2,'健身')
1
2
3
4
5
6
7
8
9
10
11
12

开始实现:

先在原型链上挂上我们自定义的 call2 方法, 让所有函数共享此方法

    Function.prototype.call2 = function() {
      console.log(this)
    }
    setDetails.call2()
1
2
3
4

call2 中通过 this 拿到调用 call2 的原函数, 接下来通过上面提到 当前函数被调用时, 它是作为某个对象的方法 (前面加了点'.') this指向这个对象 (点 '.'前面的对象) 改变原函数中 this 的指向

    function setDetails() {
      this.name = '大毛'
    }


    Function.prototype.call2 = function(context) {
      // this === 原函数
      console.log(this)

      // 将原函数作为cat1的方法调用
      context.setDetails = this
      context.setDetails()
    }
    let cat1 = {}
    setDetails.call2(cat1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

继续改进

  • 其实将原函数作为context的方法调用时, 方法名并不影响功能, 将方法名写死反而会造成方法名冲突. 在ES6可以用 Symbol 来解决, 如果不用 Symbol 可以随机生成一个基本不可能冲突的字符串, 万一冲突则继续生成到不冲突为止
  • 在方法调用后删除方法, 避免给context增加多余的方法
  • 原函数可能有返回值, 要将改变this并调用后的返回值也返回
  <script>
    function setDetails() {
      this.name = '大毛'
      return 123
    }


    Function.prototype.call2 = function(context) {
      function mySymbol(obj) {
        let unique = (Math.random + new Date())
        if(obj.hasOwnProperty(unique)) {
          return mySymbol(obj)
        }else {
          return unique
        }
      }

      let uniqueName = mySymbol(context)

      context[uniqueName] = this
      let result = context[uniqueName]()

      // 删除临时方法
      delete context[uniqueName]
      return result
    }
    let cat1 = {}
    let result = setDetails.call2(cat1)
    console.log(result)
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

改进解决参数传递的问题:

  • 因为不知道用户在调用时传参的个数, 解决可以通过 arguments 或者 剩余参数 来获取除了context剩余的参数, 将剩余参数传递给 context[uniqueName]
  <script>
    function setDetails(name,color) {
      this.name = name
      this.color = color
    }


    Function.prototype.call2 = function(context) {
      function mySymbol(obj) {
        let unique = (Math.random + new Date())
        if(obj.hasOwnProperty(unique)) {
          return mySymbol(obj)
        }else {
          return unique
        }
      }

      let uniqueName = mySymbol(context)
      
      // 获取除了第一个参数外剩余的参数
      let args = Array.from(arguments).slice(1)

      // this === 原函数
      // 将原函数作为 cat1的方法调用
      context[uniqueName] = this
      // 使用扩展运算符传参, 可以解决参数不确定的问题
      let result = context[uniqueName](...args) // 等同于  context[uniqueName]('大毛', '橘色')

      // 删除临时方法
      delete context[uniqueName]
      return result
    }
    let cat1 = {}
    let result = setDetails.call2(cat1, '大毛','橘色')
  </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

将剩余参数传递给 context[uniqueName] 还可以通过 eval 来拼接调用语句

    <script>
      function setDetails(name, color) {
        this.name = name;
        this.color = color;
      }

      Function.prototype.call2 = function (context) {
        function mySymbol(obj) {
          let unique = Math.random + new Date();
          if (obj.hasOwnProperty(unique)) {
            return mySymbol(obj);
          } else {
            return unique;
          }
        }

        let uniqueName = mySymbol(context);
        // 获取除了第一个参数外剩余的参数
        let args = [];
        for (let i = 1; i < arguments.length; i++) {
          args.push("arguments[" + i + "]");
        }

        // 将原函数作为 cat1 的方法调用
        context[uniqueName] = this;

        // 使用扩展运算符传参, 可以解决参数不确定的问题
        let result = eval("context[uniqueName](" + args.join(",") + ")");

        // 删除临时方法
        delete context[uniqueName];
        return result;
      };
      let cat1 = {};
      let result = setDetails.call2(cat1, "大毛", "橘色");
    </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

# 实现apply

apply与call功能一致, 但是在调用方式上, 是将剩余的实参以一个数组的方式传参:

实现与call基本一致, 注意兼容 apply 第二个参数没有传入的情况:

使用扩展运算符

    <script>
      function setDetails(name, color) {
        this.name = name;
        this.color = color;
      }

      Function.prototype.apply2 = function (context, args) {
        function mySymbol(obj) {
          let unique = Math.random + new Date();
          if (obj.hasOwnProperty(unique)) {
            return mySymbol(obj);
          } else {
            return unique;
          }
        }

        let uniqueName = mySymbol(context);
        // 获取除了第一个参数外剩余的参数
        let arr = [];
        if (args) {
          for (let i = 0; i < args.length; i++) {
            arr.push("args[" + i + "]");
          }
        }

        // 将原函数作为 cat1 的方法调用
        context[uniqueName] = this;

        // 使用扩展运算符传参, 可以解决参数不确定的问题
        let result = eval("context[uniqueName](" + arr.join(",") + ")");

        // 删除临时方法
        delete context[uniqueName];
        return result;
      };
      let cat1 = {};
      let result = setDetails.apply2(cat1, ["大毛", "橘色"]);
    </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

# 实现bind

bindcallapply的功能类似, 但有不同, bind不会立即调用函数, 只做this的绑定, 并且返回一个新的函数, 这个函数运行的逻辑与原函数一致, 但是this会指向之前绑定的对象

    function setDetails(name, color) {
      this.name = name
      this.color = color
    }
    let cat1 = {}
    let setDetails2 = setDetails.bind(cat1)
    setDetails2('大毛', '橘色')
1
2
3
4
5
6
7

实现思路: bind返回一个函数, 在函数体内调用 apply

      Function.prototype.bind2 = function (context) {
        let originFn = this;
        return function () {
          originFn.apply(context);
        };
      };
1
2
3
4
5
6

继续改进:

调用 bind 后返回的函数是可以传参的

bind方法除了第一个参数, 还可以额外传参, 可以理解为预传参

    function setDetails3(name, color) {
      this.name = name
      this.color = color
    }
    let cat2 = {}
    let setDetails4 = setDetails3.bind(cat2,'大毛')
    setDetails4('橘色')
1
2
3
4
5
6
7

将第一次传参先存起来, 在调用将第一次和第二次传参进行拼接

      Function.prototype.bind2 = function (context) {
        let originFn = this;
        let args = Array.from(arguments).slice(1)
        return function () {
          let bindArgs = Array.from(arguments)
          return originFn.apply(context, args.concat(bindArgs));
        };
      };

      function setDetails(name, color) {
        this.name = name;
        this.color = color;
        return 123
      }
      let cat1 = {};
      let setDetails2 = setDetails.bind2(cat1, '大毛');
      let result = setDetails2('橘色');
      console.log(result)  // 123
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

最后改进:

  • bind 返回的函数, 如果之后是作为构造函数调用, 则原函数中的this会指向创建的对象, 而不会指向之前绑定的对象, 并且生成的实例仍然可以使用原型对象上的属性
      function Cat(name, color) {
        this.name = name
        this.color = color
      }
      Cat.prototype.miao = function() {
        console.log('瞄~!')
      }
      let cat1 = {}
      let CatBind = Cat.bind(cat1)
      let cat2 = new CatBind('大毛','橘色')
      cat2.miao()
1
2
3
4
5
6
7
8
9
10
11

当返回的函数作为构造函数, 通过 new xxx() 调用时, 返回的函数的this指向并返回函数作为构造生成的类例, 因此需要判断this instanceof 返回的函数, 另外, 返回的函数要继承原函数 (将原型链连接起来)

    <script>
      Function.prototype.bind2 = function (context) {
        let originFn = this;
        let args = Array.from(arguments).slice(1);
        function fBind() {
          let bindArgs = Array.from(arguments);
          return originFn.apply(this instanceof fBind /*是否作为构造函数调用*/ ? this:context, args.concat(bindArgs));
        };
        fBind.prototype = Object.create(this.prototype)
        return fBind
      };

      function Cat(name, color) {
        this.name = name;
        this.color = color;
      }
      Cat.prototype.miao = function () {
        console.log("瞄~!");
      };
      let cat2 = {};
      let CatBind = Cat.bind2(cat2);
      let cat3 = new CatBind("大毛", "橘色");
      cat3.miao();
    </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
上次更新: 2020/11/24 下午2:27:13