call、apply和bind

前言

JS 专门为我们提供了一些方法来改变函数内部的 this 指向。常见的方法有
call()、apply()、bind() 方法。

call() 方法

call() 方法的作用

call()
方法的作用:可以调用一个函数,与此同时,它还可以改变这个函数内部的
this 指向。

call()
方法的另一个应用:可以实现继承。之所以能实现继承,其实是利用了上面的作用。

语法:

fn1.call(想要将this指向哪里, 函数实参1, 函数实参2);

备注:第一个参数中,如果不需要改变 this 指向,则传 null。

call() 方法举例

举例 1、通过 call() 调用函数:

const obj1 = {

nickName: ‘qianguyihao’,

age: 28,

};

function fn1() {

console.log(this);

console.log(this.nickName);

}

fn1.call(this); // this的指向并没有被改变,此时相当于 fn1();

上方代码的打印结果:

window

undefined

上面的代码,跟普通的函数调用 fn1() 没有区别。

举例 2、通过 call() 改变 this 指向:

var obj1 = {

nickName: ‘qianguyihao’,

age: 28,

};

function fn1(a, b) {

console.log(this);

console.log(this.nickName);

console.log(a + b);

}

fn1.call(obj1, 2, 4); // 先将 this 指向 obj1,然后执行 fn1() 函数

上方代码的打印结果:

obj1

qianguyihao

6

举例 3、通过 call() 实现继承:

// 给 Father 增加 name 和 age 属性

function Father(myName, myAge) {

this.name = myName;

this.age = myAge;

}

function Son(myName, myAge) {

// 【下面这一行,重要代码】

// 通过这一步,将 father 里面的 this 修改为 Son 里面的 this;另外,给
Son 加上相应的参数,让 Son 自动拥有 Father 里的属性。最终实现继承

Father.call(this, myName, myAge);

}

const son1 = new Son(‘千古壹号’, 28);

console.log(JSON.stringify(son1));

上方代码中,通过 call() 方法,让 Son 继承了 Father 里面的 name 和 age
属性。

打印结果:

{“myName”:”千古壹号”,”myAge”:28}

apply() 方法

apply() 方法的作用

apply()
方法的作用:可以调用一个函数,与此同时,它还可以改变这个函数内部的
this 指向。这一点,和 call()类似。

apply() 方法的应用: 由于
apply()需要传递数组,所以它有一些巧妙应用,稍后看接下来的应用举例就知道了。

语法:

fn1.apply(想要将this指向哪里, [函数实参1, 函数实参2]);

备注:第一个参数中,如果不需要改变 this 指向,则传 null。

到这里可以看出, call() 和 apply()
方法的作用是相同的。唯一的区别在于,apply()
里面传入的实参,必须是数组(或者维数组)

apply() 方法举例

举例、通过 apply() 改变 this 指向:

var obj1 = {

nickName: ‘qianguyihao’,

age: 28,

};

function fn1(a) {

console.log(this);

console.log(this.nickName);

console.log(a);

}

fn1.apply(obj1, [‘hello’]); // 先将 this 指向 obj1,然后执行 fn1()
函数

注意,上方代码中,call()
里面传实参时,需要以数组的形式。即便是传一个实参,也需要传数组。

打印结果:

obj1

qianguyihao

hello

apply() 方法的巧妙应用:求数组的最大值

我们知道,如果想要求数组中元素的最大值的时候,数组本身是没有自带方法的。那怎么办呢?

虽然数组里没有获取最大值的方法,但是数值里面有 Math.max(数字1,数字2,数字3) 方法,可以获取多个数值中的最大值
另外,由于 apply() 方法在传递实参时,必须要以数组的形式,所以我们可以
通过 Math.max() 和 apply() 曲线救国。

举例:求数组中多个元素的最大值:

const arr1 = [3, 7, 10, 8];

// 下面这一行代码的目的,无需改变 this 指向,所以:第一个参数填
null,或者填 Math,或者填 this 都可以。严格模式中,不让填null。

const maxValue = Math.max.apply(Math, arr1); // 求数组 arr1
中元素的最大值

console.log(maxValue);

const minValue = Math.min.apply(Math, arr1); // 求数组 arr1
中元素的最小值

console.log(minValue);

打印结果:

10

3

bind() 方法

bind() 方法的作用

bind() 方法不会调用函数,但是可以改变函数内部的 this 指向。

把call()、apply()、bind()这三个方法做一下对比,你会发现:实际开发中,
bind()
方法使用得最为频繁。如果有些函数,我们不需要立即调用,但是又想改变这个函数内部的this指向,此时用
bind() 是最为合适的。

语法:

新函数 = fn1.bind(想要将this指向哪里, 函数实参1, 函数实参2);

参数:

  • 第一个参数:在 fn1 函数运行时,指定 fn1 函数的this
    指向。如果不需要改变 this 指向,则传 null。

  • 其他参数:fn1 函数的实参。

解释:它不会调用 fn1 函数,但会返回 由指定this
和指定实参的原函数拷贝。可以看出, bind() 方法是有返回值的。

bind() 方法举例

bind 的第一个参数会作为原函数运行时的 this
指向,不多说;而第二个开始的参数是可选的,当绑定函数被调用时,这些参数加上绑定函数本身的参数会按照顺序作为原函数运行时的参数。怎么理解?

function fn(a, b, c) {

return a + b + c;

}

var _fn = fn.bind(null, 10);

var ans = _fn(20, 30); // 60

fn 函数需要三个参数,_fn 函数将 10
作为默认的第一个参数,所以只需要传入两个参数即可,如果你不小心传入了三个参数,放心,也只会取前两个。

function fn(a, b, c) {

return a + b + c;

}

var _fn = fn.bind(null, 10);

var ans = _fn(20, 30, 40); // 60

这有啥用呢?如果某些函数,前几个参数已经 “内定” 了,我们便可以用 bind
返回一个新的函数。也就是说,bind()
能使一个函数拥有预设的初始参数。这些参数(如果有的话)作为 bind()
的第二个参数跟在 this
后面,之后它们会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们的后面。

function list() {

return Array.prototype.slice.call(arguments);
//slice不传参数的时候就是造一个完整的数组出来

}

var list1 = list(1, 2, 3); // [1, 2, 3]

// Create a function with a preset leading argument

var leadingThirtysevenList = list.bind(undefined, 37);

var list2 = leadingThirtysevenList(); // [37]

var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]

new

使用 bind 返回的结果还是个 function,是个 function 就可以被 new
运算符调用,那么结果呢?规范中说的很清楚了,当使用 new
操作符调用绑定函数时,bind 的第一个参数无效。

function Person(name, age) {

this.name = name;

this.age = age;

}

var _Person = Person.bind({});

var p = new _Person(‘hanzichi’, 30); // Person {name: “hanzichi”,
age: 30}

一般我们不会去这么用,但是如果要写个 bind 的
polyfill( http://caniuse.com/#search=bind ),还是需要考虑用 new
调用的情况。

我们也可以设置默认值(参考上一小节),原先提供的那些参数仍然会被前置到构造函数调用的前面。

function Person(name, age) {

this.name = name;

this.age = age;

}

var _Person = Person.bind(null, ‘hanzichi’);

var p = new _Person(30); // Person {name: “hanzichi”, age: 30}

配合 setTimeout

什么时候容易丢失 this 指向?恩,setTimeout 是一个场景,很容易把 this
指向 window,当然,setInterval 也是一样。当使用对象的方法时,需要 this
引用对象,你可能需要显式地把 this 绑定到回调函数以便继续使用对象。

var canvas = {

render: function() {

this.update();

this.draw();

},

update: function() {

// …

},

draw: function() {

// …

}

};

window.setInterval(canvas.render, 1000 / 60);

用 canvas
写特效或者做游戏时经常会碰到类似的问题。上面的代码是有问题的,render
方法中的 this 其实被指向了 window!我们可以用 bind, 显式地把 this
绑定到回调函数以便继续使用该对象。

window.setInterval(canvas.render.bind(canvas), 1000);

类似的情况还有 dom 的事件监听,一不小心可能 this 就被指向了 dom
元素。可以参考下以前做 bigrender
时写的这部分代码 https://github.com/hanzichi/hanzichi.github.io/blob/master/2016/bigrender/js/bigrender.js#L179-L184 。

tip

bind 还能做一些有意思的事情。

通常来说,将一个类数组转为数组,我们会用 slice(ie9- 不支持)。参考#14

var slice = Array.prototype.slice;

// slice.apply(arguments);

// slice(arguments, 1);

bind 能让调用变的更加简单。

// same as “slice” in the previous example

var unboundSlice = Array.prototype.slice;

var slice = Function.prototype.call.bind(unboundSlice);

// …

slice(arguments);

// slice(arguments, 1);

再举个类似的例子,比如说我们要添加事件到多个节点,for
循环当然没有任何问题,我们还可以 “剽窃” forEach 方法:

Array.prototype.forEach.call(document.querySelectorAll(‘input[type=”button”]’),
function(el){

el.addEventListener(‘click’, fn);

});

更进一步,我们可以用 bind 将函数封装的更好:

var unboundForEach = Array.prototype.forEach

, forEach = Function.prototype.call.bind(unboundForEach);

forEach(document.querySelectorAll(‘input[type=”button”]’),
function (el) {

el.addEventListener(‘click’, fn);

});

同样类似的,我们可以将 x.y(z) 变成 y(x,z) 的形式:

var obj = {

num: 10,

getCount: function() {

return this.num;

}

};

var unboundBind = Function.prototype.bind

, bind = Function.prototype.call.bind(unboundBind);

var getCount = bind(obj.getCount, obj);

console.log(getCount()); // 10

再举个栗子。每隔一秒在控制台打印 1-5,看起来是道考察闭包的经典题目。

for(var i = 1; i <= 5; i++) {

!function(i) {

setTimeout(function() {

console.log(i);

}, i * 1000);

}(i);

}

ES6 下能用 let :

for(let i = 1; i <= 5; i++) {

setTimeout(function() {

console.log(i);

}, i * 1000);

}

也可以用 bind,瞬间逼格提升:

for(var i = 1; i <= 5; i++) {

setTimeout(console.log.bind(console, i), i * 1000);

}


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!