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 bind #7

Open
axuebin opened this issue Oct 16, 2017 · 4 comments
Open

JavaScript基础心法——call apply bind #7

axuebin opened this issue Oct 16, 2017 · 4 comments

Comments

@axuebin
Copy link
Owner

axuebin commented Oct 16, 2017

整理callapplybind这三个方法的的知识点。


之前这篇文章提到过this的各种情况,其中有一种情况就是通过callapplybind来将this绑定到指定的对象上。

也就是说,这三个方法可以改变函数体内部this的指向。

这三个方法有什么区别呢?分别适合应用在哪些场景中呢?

先举个简单的栗子 ~

var person = {
  name: "axuebin",
  age: 25
};
function say(job){
  console.log(this.name+":"+this.age+" "+job);
}
say.call(person,"FE"); // axuebin:25 FE
say.apply(person,["FE"]); // axuebin:25 FE
var sayPerson = say.bind(person,"FE");
sayPerson(); // axuebin:25 FE

对于对象person而言,并没有say这样一个方法,通过call/apply/bind就可以将外部的say方法用于这个对象中,其实就是将say内部的this指向person这个对象。

call

call是属于所有Function的方法,也就是Function.prototype.call

The call() method calls a function with a given this value and arguments provided individually.

call() 方法调用一个函数, 其具有一个指定的this值和分别地提供的参数(参数的列表)。

它的语法是这样的:

fun.call(thisArg[,arg1[,arg2,]]);

其中,thisArg就是this指向,arg是指定的参数。

call的用处简而言之就是可以让call()中的对象调用当前对象所拥有的function。

ECMAScript规范

ECMAScript规范中是这样定义call的:

当以thisArg和可选的arg1,arg2等等作为参数在一个func对象上调用call方法,采用如下步骤:

  1. 如果IsCallable(func)false, 则抛出一个TypeError异常。
  2. argList为一个空列表。
  3. 如果调用这个方法的参数多余一个,则从arg1开始以从左到右的顺序将每个参数插入为argList的最后一个元素。
  4. 提供thisArg作为this值并以argList作为参数列表,调用func[[Call]]内部方法,返回结果。

call方法的length属性是1。

在外面传入的thisArg值会修改并成为this值。thisArgundefinednull时它会被替换成全局对象,所有其他值会被应用ToObject并将结果作为this值,这是第三版引入的更改。

使用call调用函数并且指定this

var obj = {
  a: 1
}
function foo(b, c){
  this.b = b;
  this.c = c;
  console.log(this.a + this.b + this.c);
}
foo.call(obj,2,3); // 6

call实现继承

在需要实现继承的子类构造函数中,可以通过call调用父类构造函数实现继承。

function Person(name, age){
  this.name = name;
  this.age = age;
  this.say = function(){
    console.log(this.name + ":" + this.age);
  }
}
function Student(name, age, job){
  Person.call(this, name ,age);
  this.job = job;
  this.say = function(){
    console.log(this.name + ":" + this.age + " " + this.job);
  }
}
var me = new Student("axuebin",25,"FE");
console.log(me.say()); // axuebin:25 FE

apply

apply也是属于所有Function的方法,也就是Function.prototype.apply

The apply() method calls a function with a given this value, and arguments provided as an array (or an array-like object).

apply() 方法调用一个函数, 其具有一个指定的this值,以及作为一个数组(或类似数组的对象)提供的参数。

它的语法是这样的:

fun.apply(thisArg, [argsArray]);

其中,thisArg就是this指向,argsArray是指定的参数数组。

通过语法就可以看出callapply的在参数上的一个区别:

  • call的参数是一个列表,将每个参数一个个列出来
  • apply的参数是一个数组,将每个参数放到一个数组中

ECMAScript规范

当以thisArgargArray为参数在一个func对象上调用apply方法,采用如下步骤:

  1. 如果IsCallable(func)false, 则抛出一个TypeError异常 .
  2. 如果argArraynullundefined, 则
  3. 返回提供thisArg作为this值并以空参数列表调用func[[Call]]内部方法的结果。
  4. 如果Type(argArray)不是Object, 则抛出一个TypeError异常 .
  5. len为以"length"作为参数调用argArray[[Get]]内部方法的结果。
  6. nToUint32(len).
  7. argList为一个空列表 .
  8. index为0.
  9. 只要index<n就重复
  10. indexNameToString(index).
  11. nextArg为以indexName作为参数调用argArray[[Get]]内部方法的结果。
  12. nextArg作为最后一个元素插入到argList里。
  13. 设定indexindex + 1.
  14. 提供thisArg作为this值并以argList作为参数列表,调用func[[Call]]内部方法,返回结果。

apply方法的length属性是 2。

在外面传入的thisArg值会修改并成为this值。thisArgundefinednull时它会被替换成全局对象,所有其他值会被应用ToObject并将结果作为this值,这是第三版引入的更改。

用法

在用法上applycall一样,就不说了。

实现一个apply

参考链接:jawil/blog#16

第一步,绑定上下文

Function.prototype.myApply=function(context){
  // 获取调用`myApply`的函数本身,用this获取
  context.fn = this;
  // 执行这个函数
  context.fn();
  // 从上下文中删除函数引用
  delete context.fn;
}

var obj ={
  name: "xb",
  getName: function(){
    console.log(this.name);
  }
}

var me = {
  name: "axuebin"
}

obj.getName(); // xb 
obj.getName.myApply(me); // axuebin

确实成功地将this指向了me对象,而不是本身的obj对象。

第二步,给定参数

上文已经提到apply需要接受一个参数数组,可以是一个类数组对象,还记得获取函数参数可以用arguments吗?

Function.prototype.myApply=function(context){
  // 获取调用`myApply`的函数本身,用this获取
  context.fn = this;
  // 通过arguments获取参数
  var args = arguments[1];
  // 执行这个函数,用ES6的...运算符将arg展开
  context.fn(...args);
  // 从上下文中删除函数引用
  delete context.fn;
}

var obj ={
  name: "xb",
  getName: function(age){
    console.log(this.name + ":" + age);
  }
}

var me = {
  name: "axuebin"
}

obj.getName(); // xb:undefined
obj.getName.myApply(me,[25]); // axuebin:25

context.fn(...arg)是用了ES6的方法来将参数展开,如果看过上面那个链接,就知道这里不通过...运算符也是可以的。

原博主通过拼接字符串,然后用eval执行的方式将参数传进context.fn中:

for (var i = 0; i < args.length; i++) {
  fnStr += i == args.length - 1 ? args[i] : args[i] + ',';
}
fnStr += ')';//得到"context.fn(arg1,arg2,arg3...)"这个字符串在,最后用eval执行
eval(fnStr); //还是eval强大

第三步,当传入apply的this为null或者为空时

我们知道,当apply的第一个参数,也就是this的指向为null时,this会指向window。知道了这个,就简单了~

Function.prototype.myApply=function(context){
  // 获取调用`myApply`的函数本身,用this获取,如果context不存在,则为window
  var context = context || window;
  context.fn = this;
  //获取传入的数组参数
  var args = arguments[1];
  if (args == undefined) { //没有传入参数直接执行
    // 执行这个函数
    context.fn()
  } else {
    // 执行这个函数
    context.fn(...args);
  }
  // 从上下文中删除函数引用
  delete context.fn;
}

var obj ={
  name: "xb",
  getName: function(age){
    console.log(this.name + ":" + age);
  }
}

var name = "window.name";

var me = {
  name: "axuebin"
}

obj.getName(); // xb:25
obj.getName.myApply(); // window.name:undefined
obj.getName.myApply(null, [25]); // window.name:25
obj.getName.myApply(me, [25]); // axuebin:25

第四步 保证fn函数的唯一性

ES6中新增了一种基础数据类型Symbol

const name = Symbol();
const age = Symbol();
console.log(name === age); // false

const obj = {
  [name]: "axuebin",
  [age]: 25
}

console.log(obj); // {Symbol(): "axuebin", Symbol(): 25}
console.log(obj[name]); // axuebin

所以我们可以通过Symbol来创建一个属性名。

var fn = Symbol();
context[fn] = this;

完整的apply

Function.prototype.myApply=function(context){
  // 获取调用`myApply`的函数本身,用this获取,如果context不存在,则为window
  var context = context || window;
  var fn = Symbol();
  context[fn] = this;
  //获取传入的数组参数
  var args = arguments[1];
  if (args == undefined) { //没有传入参数直接执行
    // 执行这个函数
    context[fn]()
  } else {
    // 执行这个函数
    context[fn](...args);
  }
  // 从上下文中删除函数引用
  delete context.fn;
}

这样就是一个完整的apply了,我们来测试一下:

var obj ={
  name: "xb",
  getName: function(age){
    console.log(this.name + ":" + age);
  }
}

var name = "window.name";

var me = {
  name: "axuebin"
}

obj.getName(); // xb:25
obj.getName.myApply(); // window.name:undefined
obj.getName.myApply(null, [25]); // window.name:25
obj.getName.myApply(me, [25]); // axuebin:25

ok 没啥毛病 ~

再次感谢1024大佬 ~

bind

The bind() method creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.

bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

语法:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

其中,thisArg就是this指向,arg是指定的参数。

可以看出,bind会创建一个新函数(称之为绑定函数),原函数的一个拷贝,也就是说不会像callapply那样立即执行。

当这个绑定函数被调用时,它的this值传递给bind的一个参数,执行的参数是传入bind的其它参数和执行绑定函数时传入的参数。

用法

当我们执行下面的代码时,我们希望可以正确地输出name,然后现实是残酷的

function Person(name){
  this.name = name;
  this.say = function(){
    setTimeout(function(){
      console.log("hello " + this.name);
    },1000)
  }
}
var person = new Person("axuebin");
person.say(); //hello undefined

这里this运行时是指向window的,所以this.nameundefined,为什么会这样呢?看看MDN的解释:

由setTimeout()调用的代码运行在与所在函数完全分离的执行环境上。这会导致,这些代码中包含的 this 关键字在非严格模式会指向 window。

有一个常见的方法可以使得正确的输出:

function Person(name){
  this.name = name;
  this.say = function(){
    var self = this;
    setTimeout(function(){
      console.log("hello " + self.name);
    },1000)
  }
}
var person = new Person("axuebin");
person.say(); //hello axuebin

没错,这里我们就可以用到bind了:

function Person(name){
  this.name = name;
  this.say = function(){
    setTimeout(function(){
      console.log("hello " + this.name);
    }.bind(this),1000)
  }
}
var person = new Person("axuebin");
person.say(); //hello axuebin

MDN的Polyfill

Function.prototype.bind = function (oThis) {
  var aArgs = Array.prototype.slice.call(arguments, 1)
  var fToBind = this;
  var fNOP = function () {}
  var fBound = function () {
    fBound.prototype = this instanceof fNOP ? new fNOP() : fBound.prototype;
    return fToBind.apply(this instanceof fNOP ? this : oThis || this, aArgs )
  }   
  if( this.prototype ) {
    fNOP.prototype = this.prototype;
  }
  return fBound;
}

总结

  • 三者都是用来改变函数的this指向
  • 三者的第一个参数都是this指向的对象
  • bind是返回一个绑定函数可稍后执行,callapply是立即调用
  • 三者都可以给定参数传递
  • call给定参数需要将参数全部列出,apply给定参数数组

感谢

不用call和apply方法模拟实现ES5的bind方法

深入浅出妙用 Javascript 中 apply、call、bind

回味JS基础:call apply 与 bind

@axuebin axuebin changed the title JavaScript call apply bind JavaScript的call apply bind Oct 26, 2017
@axuebin axuebin changed the title JavaScript的call apply bind JavaScript基础心法——call apply bind Oct 26, 2017
@HolyHeart
Copy link

obj.getName(); // xb:25 这里不应该是 xb: undefined吗?

@YSherlock
Copy link

可能漏了一种情况老哥,apply有可能有返回值,可以用result = eval('context.fn')来接收

@chunfYan
Copy link

chunfYan commented Dec 9, 2022

讲的真棒!好兄弟

@QC2168
Copy link

QC2168 commented Feb 28, 2023

apply函数的实现最后遗漏了返回执行后的值

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants