Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

原生JavaScript模拟实现call、apply #9

Open
wanCheng7 opened this issue Dec 9, 2019 · 0 comments
Open

原生JavaScript模拟实现call、apply #9

wanCheng7 opened this issue Dec 9, 2019 · 0 comments

Comments

@wanCheng7
Copy link
Owner

wanCheng7 commented Dec 9, 2019

前言

大家都知道,call, apply, bind都是用来改变一个方法的this指向的,只是参数和调用方式有区别而已,如果对这一块还不了解的可以自己查一下。

那不知道大家想过没有,这几个方法内部是如何实现的呢,我们试着用原生js来实现一下。

call和apply方法到底做了什么

首先看一段代码:

var myObj = {
  name: 'wan',
  sayHello: function(age) {
    console.log('hello, my name is ' + this.name + ', I`m ' + age + ' years old');
  }
}
var test = {
  name: 'cheng'
}
myObj.sayHello(25);  //  hello, my name is wan, I`m 25 years old
// 使用call和apply改变this指向
myObj.sayHello.call(test, 25);  // hello, my name is cheng, I`m 25 years old
myObj.sayHello.apply(test, [25]);  // hello, my name is cheng, I`m 25 years old

查文档可知,callapply的语法分别是:

function.call(thisArg, arg1, arg2, ...)
func.apply(thisArg, [argsArray])

经过观察可知,callapply的作用实际上就是在目标thisArg里运行了一遍function,然后将这个方法删除了。所以我们模拟实现一个自己的call的思路可以是在目标上下文环境的thisArg里 添加一个唯一属性,指向我们调用的方法,执行之后再删除。

实现一个自己的call

先实现一个自己的callcallOne,因为callfunction原型对象上的一个方法,所以可以依据上面的思路重写一个自己的call:

第一个版本:

Function.prototype.callOne = function(context) {
  context.fn = this;
  context.fn();
  delete context.fn
}
myObj.sayHello.callOne(test, 25); //  hello, my name is cheng, I`m undefined years old

好,经过第一个版本,我们成功改变了this的指向,可是参数还没做处理,那下个版本就对参数处理一下吧。

第二个版本:

var myObj = {
  name: 'wan',
  sayHello: function(age, color) {
    console.log(this.name, age, color);
  }
}
var test = {
  name: 'cheng'
}
Function.prototype.callTwo = function(context) {
  context.fn = this;
  var args = arguments;
  var len = args.length;
  var fnStr = 'context.fn(';
  for (var i = 1; i < len; i++) {
    fnStr += (i == len-1) ? 'args[' + i + ']' : 'args['+i+']' + ',';
  }
  fnStr += ')';
  eval(fnStr)
  delete context.fn
}
myObj.sayHello.callTwo(test, 25, "red");  // cheng 25 red

特别注意一下,循环里面之所以要用string,是为了在eval中最后再执行,不然参数传字符串时会有问题。

第三个版本:

经过对参数的处理,好像差不多了呢,现在传多个不同类型的参数也是可以的了。那接下来据解决一下极端情况吧:

  • thisArg这个参数时可以不传或者传null的,这种情况下'thisArg'是指向window的。
  • 我们的模拟中修改了context中的fn属性,并且还删除了,这是有隐患的,万一本来它就有这个属性那就修改了context的值了,这肯定是不行的。所以我们可以自己造一个类似于ES6中的symbol类型的唯一属性作为context的过渡属性。

所以针对上面两个问题进行一下优化:

var myObj = {
  name: 'wan',
  sayHello: function(age, color) {
    console.log(this.name, age, color);
  }
}
var test = {
  name: 'cheng'
}
function getSymbolProperty(obj) {
  var uniquePro = '__' + Math.random().toFixed(5);
  if(obj.hasOwnProperty(uniquePro)) {
    getSymbolProperty(obj)
  }
  else{
    return uniquePro;
  }
}
Function.prototype.callThree = function(context) {
  var context = context || window;
  var fn = getSymbolProperty(context);
  context[fn] = this;
  var args = arguments;
  var len = args.length;
  var fnStr = 'context[fn](';
  for (var i = 1; i < len; i++) {
    fnStr += (i == len-1) ? 'args['+i+']' : 'args['+i+']' + ',';
  }
  fnStr += ')';
  eval(fnStr)
  delete context[fn]
}
myObj.sayHello.callThree(test, 25, "red");  // cheng 25 red

到这一步,对于call的模拟实现就基本完成了。

实现一个自己的apply

applycall差不多,只是参数不同而已,所以唯一的区别就是对于参数的处理改变一下就行了。

var myObj = {
  name: 'wan',
  sayHello: function(age, color) {
    console.log(this.name, age, color);
  }
}
var test = {
  name: 'cheng'
}
function getSymbolProperty(obj) {
  var uniquePro = '__' + Math.random().toFixed(5);
  if(obj.hasOwnProperty(uniquePro)) {
    getSymbolProperty(obj)
  }
  else{
    return uniquePro;
  }
}
Function.prototype.applyThree = function(context, arr) {
  var context = context || window;
  var fn = getSymbolProperty(context);
  context[fn] = this;

  var result;
  if(arr && arr.length) {
    var args = [];
    for (let i = 0; i < arr.length; i++) {
      args.push('arr[' + i + ']');
    }
    result = eval('context[fn](' + args + ')');
  }
  else{
    result = context[fn]();
  }
  delete context[fn]
  return result
}
myObj.sayHello.applyThree(test, [25, 'red']);  // cheng 25 red

实现一个自己的bind

bindcall最大的区别就是不会立即运行,返回的是一个函数。

参考

@wanCheng7 wanCheng7 changed the title 原生JavaScript实现apply、call、bind 原生JavaScript模拟实现apply、call、bind Dec 10, 2019
@wanCheng7 wanCheng7 changed the title 原生JavaScript模拟实现apply、call、bind 原生JavaScript模拟实现call、apply、bind Dec 10, 2019
@wanCheng7 wanCheng7 changed the title 原生JavaScript模拟实现call、apply、bind 原生JavaScript模拟实现call、apply Dec 12, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant