前言
call()、apply()和bind()方法 三者作用都是 改变this指向。
本文旨在探讨三者之间的区别和作用。
call, apply, bind 三者的区别在哪里
什么情况下用apply,什么情况下用call
apply的其他巧妙用法(一般在什么情况下可以使用apply)
bind、call、apply都是用来指定一个函数内部的this的值, 先看看bind、call、apply的用法
var year = 2021
function getDate(month, day) {
return this.year + '-' + month + '-' + day
}
let obj = {year: 2022}
getDate.call(null, 3, 8) //2021-3-8
getDate.call(obj, 3, 8) //2022-3-8
getDate.apply(obj, [6, 8]) //2022-6-8
getDate.bind(obj)(3, 8) //2022-3-8
一、 call和apply
1. call() 方法
call()方法接受的语法和作用与apply()方法类似,只有一个区别就是call()接受的是一个参数列表,而apply()方法接受的是一个包含多个参数的数组。
二者都是函数对象Function的方法,且第一个参数都是要绑定对象的上下文
例如:
let obj = {
a: 1,
get: function(){
return 2
}
}
let g = obj.get
g.call({},1,2,3)
g.apply({},[1,2,3])
- call方法调用父构造函数
function Product(name, price){
this.name = name;
this.food = food;
}
// 调用父构造函数的call方法来实现继承
function Food(name, price){
Product.call(this.name, toy);
this.category = 'food';
}
function Toy(name, price){
Product.call(this, name, price);
this.category = 'toy';
}
var cheese = new Food('feta', 5);
var fun = new Toy('robot', 40);
- call方法调用匿名函数
var animals = [
{species: 'Lion', name: 'King'},
{species: 'Whale', name: 'Fail'}
];
for(var i = 0; i < animals.length; i++){
(function(i){
this.print = function(){
console.log('#' + i + ' ' + this.species + ': ' + this.name);
}
this.print();
}).call(animals[i], i); //call调用匿名函数
}
- call方法调用函数并且指定上下文的this
var obj = {
animal: 'cats', sleepDuration: '12 and 16 hours'
};
function greet(){
var reply = [this.animal, 'typically sleep between', this.sleepDuration].join(' ');
console.log(reply);
}
greet.call(obj); //"cats typically sleep between 12 and 16 hours"
- call方法调用函数并且不指定第一个参数(argument)
在这个例子中,我们没有传递第一个参数,this的值将被绑定为全局对象。
var sData = 'marshall';
function display(){
console.log("sData's value is %s",this.sData);
}
display.call(); // sData value is marshall
但是在严格模式下,this 的值将会是undefined
var sData = 'marshall';
function display(){
console.log("sData's value is %s",this.sData);
}
display.call(); // Cannot read the property of 'sData' of undefined
2. apply() 方法
使用 apply, 我们可以只写一次这个方法然后在另一个对象中继承它,而不用在新对象中重复写该方法。
apply 与 call() 非常相似,不同之处在于提供参数的方式。apply 使用参数数组而不是一组参数列表。apply 可以使用数组字面量(array literal),如 fun.apply(this, [‘eat’, ‘bananas’]),或数组对象, 如 fun.apply(this, new Array(‘eat’, ‘bananas’))。
- apply方法调用一个具有给定this值的函数,以及以一个数组的形式提供参数。
var array = ['marshall','eminem'];
var elements = [0,1,2];
array.push.apply(array,elements);
console.log(array); //['marshall','eminem',0,1,2]
- 使用apply和内置函数
- 对于一些需要写循环以遍历数组各项的需求,我们可以用apply完成以避免循环。
//找出数组中最大值和最小值
var numbers = [5, 6, 2, 3, 7];
//使用Math.min和Math.max以及apply函数时的代码
var max = Math.max.apply(null, numbers);
//var max = Math.max.apply(this, numbers);
//var max = Math.max.apply(numbers, numbers);
var min = Math.min.apply(null, numbers);
上边这种调用apply的方法,有超出JavaScript引擎参数长度上限的风险。
如果我们的参数数组非常大,推荐使用下边这种混合策略:将数组切块后循环传入目标方法
function minOfArray(arr) {
var min = Infinity;
var QUANTUM = 32768;
for (var i = 0, len = arr.length; i < len; i += QUANTUM) {
var submin = Math.min.apply(null, arr.slice(i, Math.min(i + QUANTUM, len)));
min = Math.min(submin, min);
}
return min;
}
var min = minOfArray([5, 6, 2, 3, 7]);
3、bind
bind()函数会创建一个新的绑定函数,这个绑定函数包装了原函数的对象。调用绑定函数通常会执行包装函数。
绑定函数内部属性:
- 包装的函数对象
- 在调用包装函数时始终作为this传递的值
- 在对包装函数做任何调用时都会优先用列表元素填充参数列表。
而原函数 retrieveX 中的 this 并没有被改变,依旧指向全局对象 window。
this.x = 9; //this指向全局的window对象
var module = {
x: 81,
getX: function(){return this.x;}
};
console.log(module.getX()); //81
var retrieveX = module.getX;
console.log(retrieveX()); //9,因为函数是在全局作用域中调用的
// 创建一个新函数,把this绑定到module对象
// 不要将全局变量 x 与 module 的属性 x 混淆
var boundGetX = retrieveX.bind(module);
console.log(boundGetX()); //81
bind传递参数问题:
在通过bind改变this指向的时候所传入的参数会拼接在调用返回函数所传参数之前,多余参数不起作用。
var newShowName = showName.bind(newThis, 'hello');
//在通过bind改变this指向的时候只传了“hello”一个参数,
//在调用newShowName这个返回参数的时候,bind传参拼接在其前
newShowName('world'); //输出:newThis hello world
var newShowName = showName.bind(newThis, 'hello');
//在通过bind改变this指向的时候只传了“hello”一个参数,
//在调用newShowName这个返回参数的时候,bind传参拼接在其前,
//这时newShowName的参数为“hello”,“a”,“world”
//而该函数只需要两个参数,则第三个参数被忽略
newShowName('a','world'); //输出:newThis hello a
bind传入的参数和newShowName方法传入的参数会拼接在一起,一齐传给showName方法。
bind无法改变构造函数的this指向
var name = 'window';
var newThis = { name: 'newThis' };
function showName(info1, info2) {
console.log(this.name, info1, info2);
}
showName('a', 'b'); //输出:window a b
// 通过bind改变this指向
var newShowName = showName.bind(newThis, 'hello','1','2');
newShowName('a','world'); //输出:newThis hello 1
console.log(new newShowName().constructor); //输出:showName函数体
可以看出,通过bind改变this指向返回函数的构造器还是最开始的showName函数。
new newShowName()实例化了一个新的方法,这个方法的this也不再指向newThis。
二、 call ,apply 和bind方法应用
1. 什么情况下用apply,什么情况下用call
在给对象参数的情况下:
如果参数的形式是数组的时候,比如apply示例里面传递了参数arguments,这个参数是数组类型,并且在调用Person的时候参数的列表是对应一致的(也就是Person和Student的参数列表前两位是一致的) 就可以采用 apply。
如果我的Person的参数列表是这样的(age,name),而Student的参数列表是(name,age,grade),这样就可以用call来实现了,也就是直接指定参数列表对应值的位置(Person.call(this,age,name,grade));
call方法:call(obj,x,y,z,…)
apply方法:apply(obj,[x,y,z])
<script type="text/javascript">
/*定义一个人类*/
function Person(name,age)
{
this.name=name;
this.age=age;
}
/*定义一个学生类*/
functionStudent(name,age,grade)
{
Person.apply(this,arguments); //Person.call(this,name,age);
this.grade=grade;
}
//创建一个学生类
var student=new Student("zhangsan",21,"一年级");
//测试
alert("name:"+student.name+"\n"+"age:"+student.age+"\n"+"grade:"+student.grade);
//大家可以看到测试结果name:zhangsan age:21 grade:一年级
//学生类里面我没有给name和age属性赋值啊,为什么又存在这两个属性的值呢,这个就是apply的神奇之处.
</script>
2. call和apply 应用场景
a. 函数之间的相互调用
function add(a,b){
alert(a+b);
}
function sub(a,b){
alert(a-b);
}
add.call(sub,5,6);
add.apply(sub,[5,6]);
//弹出11,对象替换,等等这不是函数吗?? 其实函数名是Function对象的引用。
b. 构造函数之间的调用
function Person(){
this.age = 50;
this.showAge= function(){
alert(this.age);
}
}
function Son(){
this.age = 20;
}
// 让Son也具有Person的方法
// function Son(){
// this.age = 20;
// Person.call(this);
// //Person.apply(this)
// }
var father = new Person();
var xiaoming = new Son();
father.showAge.apply(xiaoming) //立即执行显示20
father.showAge.call(xiaoming) //立即执行显示20
xiaoming.showAge(); //报错,showAge() is not a function
c. 多重继承
使用多个call 或者apply 即可。
场景1:找出一个数组的最大值或最小值,数组长度不确定
var arr = [1,2,3,.......n]
Math.min.apply(this,arr) // this可随便换,但需是一个对象
场景2:两数组合并
var arr1=new Array("1","2","3");
var arr2=new Array("4","5","6");
Array.prototype.push.apply(arr1,arr2);
console.log(arr1) //["1","2","3","4","5","6"]
Array.prototype.push.apply(arr2,arr1);
console.log(arr2) //["4","5","6","1","2","3"]
var arr = Array.prototype.concat.apply(arr1,arr2);
console.log(arr)["1","2","3","4","5","6"]
d. 类数组共用数组方法
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = Array.prototype.slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var _adder = function() {
_args.push(...arguments);
return _adder;
}
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = [].slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值,执行时已经收集所有参数为数组
var adder = function () {
var _adder = function () {
// 执行收集动作,每次传入的参数都累加到原参数
[].push.apply(_args, [].slice.call(arguments));
return _adder;
};
// 利用隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
return adder(_args);
}
}
三、源码实现
1. apply与call的实现
具体代码:
// call和apply实现方式类似,只是传参的区别
// 基本思想是把fn.call(obj,args)中的fn赋值为obj的属性,然后调用obj.fn即可实现fn中this指向的改变
Function.prototype.myCall = function(context = window){ //myCall函数的参数,没有传参默认是指向window
context.fn = this //为对象添加方法(this指向调用myCall的函数)
let args = [...arguments].slice(1) // 剩余的参数
let res = context.fn(...args) // 调用该方法,该方法this指向context
delete context.fn //删除添加的方法
return res
}
Function.prototype.myApply = function(context = window){ //myCall函数的参数,没有传参默认是指向window
context.fn = this //为对象添加方法(this指向调用myCall的函数)
let res
if(arguments[1]){ //判断是否有第二个参数
res = context.fn(...arguments[1])// 调用该方法,该方法this指向context
}else{
res = context.fn()// 调用该方法,该方法this指向context
}
delete context.fn //删除添加的方法
return res
}
// 验证
function sayName(name= 'wwx',age= 18){
this.name = name
this.age = age
console.log(this.name)
return this.age
}
var obj = {
name : 'zcf',
age:24
}
var age = sayName.myCall(obj,"wxxka",19) // 19
var age1 = sayName.myApply(obj,["wwxSSS",20]) //20
2.bind源码实现
// 1. 函数A调用bind方法 需要传递的参数o, x, y, z...
// 2. 返回新的函数B
// 3. 函数B在执行的时候 具体的功能实现实际上还是使用的A 只不过this指向变成了o 不传是window
// 4. 函数B在执行的时候 你传递参数 会拼接到x, y, z的后面 一并在内部传递给A执行
// 5. 当你以new B() 构造函数依旧是A 而且obj这个参数不会起到任何操作
Function.prototype.myBind = function(target) {
var self = this;
var args = [].slice.call(arguments, 1);
var temp = function() {};
var f = function() {
var _args = [].slice.call(arguments, 0);
return self.apply(this instanceof temp ? this : (target || window), args.concat(_args));
}
temp.prototype = self.prototype;
f.prototype = new temp();
return f;
}
// 测试
function get() {
return this.x;
}
var module = {
x: 18
}
var getX = get.myBind(module);
getX();