编写时间:2019-08-21
更新时间:2021-03-24作者:鬼小妞
备注: 本文
整理
了js改变函数的this对象的指向的apply、call、bind状态:
整理中
2021-03-24
apply、call、bind
apply、call、bind共同点
-
apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
-
apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
-
apply 、 call 、bind 三者都可以利用后续参数传参;
apply、call、bind不同点
-
bind
是偏函数型,返回对应函数,便于稍后调用;apply、call则是立即调用 。 -
call
立即调用,需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。例如:func.call(this, arg1, arg2);。(非严格模式下)当参数数量不确定时,函数内部也可以通过 arguments 这个数组来遍历所有的参数。 -
apply
立即调用,把参数放在数组里。例如:func.apply(this, [arg1, arg2])。
某个函数的仅仅需要在被调用时改变this指向,使用bind
。
某个函数需要立即执行时,参数是明确知道数量时用 call , 而不确定的时候用 apply,然后把参数 push进数组传递进去
。
bind
Function.prototype.bind(thisArg [, arg1 [, arg2, …]]) 是ES5新增的函数扩展方法,bind()返回一个新的函数对象,该函数的this被绑定到thisArg上,并向事件处理器中传入参数
MDN的解释是:
bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
bind调用实例
this.height = '155'; // 在浏览器中,this 指向全局的 "window" 对象
let user = {
height: '170',
getHeight: function() { return this.height; }
};
// 自身调用
user.getHeight(); //user调用getX,此时getX里的this指的是user
console.log(user.getHeight()); // '170'
// 赋值调用
let tianZhen = user.getHeight; // 相当于 tianZhen = function() { return this.height; }
tianZhen(); // tianZhen函数是在全局作用域中调用的,相当于window.tianZhen(),此时tianZhen = function() { return this.height; }里的this指window
console.log(tianZhen()); // '155'
// 赋值绑定调用
let bindGetH = tianZhen.bind(user); // 把 'this' 绑定到 user 对象,此时bindGetH未立即执行。相当于 tianZhen = function() { return user.height; }
bindGetH(); // 即使在全局作用域直接调用,window.tianZhen()。function() { return user.height; },得到user里的height
console.log(bindGetH()); // '170'
bind实际用途
在常见的单体模式中,通常我们会使用 _this , that , self 等保存 this ,这样我们可以在改变了上下文之后继续引用到它,vue中组件dom绑定很少会用bind,但是在react中,使用React.createClass会自动绑定每个方法的this到当前组件,但使用ES6 class或纯函数时,就要靠手动绑定this了,而此时bind就非常合适。 像这样:
1.在dom添加方法时使用bind,并传入this(需要绑定this的上下文),
<button onClick={this.test.bind(this)}>点我一下</button>
import React, {Component} from 'react'
class Greeter extends Component{
constructor (props) {
super(props)
this.state = {}
}
test (value) {
console.log(value)
}
render () {
return (
<div>
<button onClick={this.test.bind(this,3)}>点我一下</button>
</div>
)
}
}
或者这样
2.在构造函数 constructor 内绑定this,好处是仅需要绑定一次,无需在dom中绑定,避免每次渲染时都要重新绑定,函数在别处复用时也无需再次绑定。
constructor (props) { super(props) this.test=this.test.bind(this) }
import React, {Component} from 'react'
class Greeter extends Component{
constructor (props) {
super(props)
this.state = {}
this.test=this.test.bind(this)
}
test (value) {
console.log(value)
}
render () {
return (
<div>
<button onClick={this.test(3)}>点我一下</button>
</div>
)
}
}
此外,你还可以在dom添加事件时使用箭头函数进行绑定,如<button onClick={()=>{this.test()}}>点我一下</button>
,这里不再详解。
bind()的连续调用
我们可以思考一个问题,就是:如果连续 bind() 两次,亦或者是连续 bind() 三次那么输出的值是什么呢?
在下述代码里,bind() 创建了一个函数,当这个事件绑定在被调用的时候,它的 this 关键词会被设置成被传入的值(这里指调用bind()时传入的第一个参数设为thisArg)。因此,这里我们传入想要的上下文 this到 bind() 函数中。然后,当回调函数被执行的时候, this 便指向thisArg。再来一个简单的栗子:
let foo = { x: 3 }
let bar = function(){
console.log(this.x);
}
bar(); // undefined
let func = bar.bind(foo);
func(); // 3
这里我们创建了一个新的函数 func,当使用 bind() 创建一个绑定函数之后,它被执行的时候,它的 this 会被设置成 foo , 而不是像我们调用 bar() 时的全局作用域。 有个有趣的问题,如果连续 bind() 两次,亦或者是连续 bind() 三次那么输出的值是什么呢?像这样:
let foo = { x: 3 };
let sed = { x: 4 };
let fiv = { x: 5 };
let bar = function(){
console.log(this.x);
}
bar(); // undefined
let func = bar.bind(foo).bind(sed);
func(); // 3
let func2 = bar.bind(foo).bind(sed).bind(fiv);
func2(); // 3
答案是,两次都仍将输出 3 ,而非期待中的 4 和 5 。
原因是,在Javascript中,多次 bind() 是无效的。
更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的。 可以看官网的Function.prototype.bind的 Polyfill实现
apply、call
在 javascript 中,call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。
JavaScript 的一大特点是,函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。
先来一个例子:
function fruits() {}
fruits.prototype = {
color: "red",
say: function() {
console.log("My color is " + this.color);
}
}
let apple = new fruits;
apple.say(); //My color is red
但是如果我们有一个对象banana= {color : "yellow"} ,我们不想对它重新定义 say 方法,那么我们可以通过 call 或 apply 用 apple 的 say 方法:
function fruits() {}
fruits.prototype = {
color: "red",
say: function() {
console.log("My color is " + this.color);
}
}
let apple = new fruits;
apple.say(); //My color is red
let banana = {
color: "yellow"
}
apple.say.call(banana); //My color is yellow
apple.say.apply(banana); //My color is yellow
所以,可以看出 call 和 apply 是为了动态改变 this 而出现的,当一个 object 没有某个方法(本栗子中banana没有say方法),但是其他的有(本栗子中apple有say方法),我们可以借助call或apply用其它对象的方法来操作。
apply、call 的区别
对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。
-
call
立即调用,需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。例如:func.call(this, arg1, arg2);。(非严格模式下)当参数数量不确定时,函数内部也可以通过 arguments 这个数组来遍历所有的参数。 -
apply
立即调用,把参数放在数组里。例如:func.apply(this, [arg1, arg2])。
例如,有一个函数定义如下:
let func = function(arg1, arg2) {
console.log(arg1, arg2)
};
func.call(this, '1', '2');
func.apply(this, ['1', '2'])
其中 this 是你想指定的上下文,他可以是任何一个 JavaScript 对象(JavaScript 中一切皆对象),call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。 JavaScript 中,某个函数的参数数量是不固定的,因此要说适用条件的话,当你的参数是明确知道数量时用 call 。 而不确定的时候用 apply,然后把参数 push 进数组传递进去。当参数数量不确定时,函数内部也可以通过 arguments 这个数组来遍历所有的参数。 为了巩固加深记忆,下面列举一些常用用法:
数组之间追加
let array1 = [12 , "foo" , { name: "Joe" } , -2458];
let array2 = ["Doe" , 555 , 100];
Array.prototype.push.apply(array1, array2);
console.log(array1); // [12, "foo", {name: "Joe"}, -2458, "Doe", 555, 100]
console.log(array2); // ["Doe", 555, 100]
获取数组中的最大值和最小值
let numbers = [5, 458 , 120 , -215 ];
let maxInNumbers1 = Math.max.apply(Math, numbers);
let maxInNumbers2 = Math.max.call(Math,5, 458 , 120 , -215);
// 或者let maxInNumbers2 = Math.max.call(Math, ...numbers);
console.log(maxInNumbers1); //458
console.log(maxInNumbers2); //458
number 本身没有 max 方法,但是 Math 有,我们就可以借助 call 或者 apply 使用其方法。
验证是否是数组(前提是toString()方法没有被重写过)
在JavaScript里使用typeof判断数据类型,只能区分基本类型,即:number、string、undefined、boolean、object。 对于null、array、function、object来说,使用typeof都会统一返回object字符串。 要想区分对象、数组、函数、单纯使用typeof是不行的。
在JS中,可以通过Object.prototype.toString方法,判断某个对象之属于哪种内置类型。 分为null、string、boolean、number、undefined、array、function、object、date、math等。
当然也可以直接使用instanceof 操作符判断。
如下使用call演示,验证是否是数组
function isArray(obj){
return Object.prototype.toString.apply(obj) === '[object Array]' ;
// 或者 return Object.prototype.toString.call(obj) === '[object Array]' ;
}
let arrFlag = isArray(['7', '6']);
console.log(arrFlag); // true
类(伪)数组使用数组方法
Javascript中存在一种名为伪数组的对象结构。比较特别的是 arguments 对象,还有像调用 getElementsByTagName , document.childNodes 之类的,它们返回NodeList对象都属于伪数组。不能应用 Array下的 push , pop数组 等方法。
但是我们能通过 Array.prototype.slice.call 转换为真正的数组的带有 length 属性的对象,这样 domNodes 就可以应用 Array 下的所有方法了。
通过 Array.prototype.slice.call 或 Array.prototype.slice.apply 转化为标准数组
伪类数组是无法直接使用Array的内置方法,所以会抛错
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta height="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div>
<span>1</span>
<span>2</span>
</div>
</body>
<script>
let domNodes1 = Array.prototype.slice.call(document.getElementsByTagName('span'));
// 或者
let domNodes2 = Array.prototype.slice.apply(document.getElementsByTagName('span'));
console.log(domNodes1);
console.log(domNodes2);
apply、call、bind比较
那么 apply、call、bind 三者相比较,之间又有什么异同呢?
何时使用 apply、call,何时使用 bind 呢。
简单的一个栗子:
var obj = {x: 81,
};
var foo = {
getX: function() {
return this.x;
}
}
console.log(foo.getX.bind(obj)()); //81
console.log(foo.getX.call(obj)); //81
console.log(foo.getX.apply(obj)); //81
三个输出的都是81,但是注意看使用 bind() 方法的,他后面多了对括号。
也就是说,区别是,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会立即执行函数。
再总结一下:
apply 、 call 、bind 三者都是用来改变函数的this对象的指向的; apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文; apply 、 call 、bind 三者都可以利用后续参数传参;
bind是返回对应函数,便于稍后调用;apply、call则是立即调用 。