作用
改变函数执行时的上下文(改变函数运行时的this指向)
引子
// 写一个构造函数类
function Person(name){
this.name = name;
}
Person.prototype = {
constructor: Person,
showName: function(){
console.log(this.name);
}
}
// 用该类new出的新对象拥有此类的属性和方法
var person = new Person('wuqinhao');
person.showName();
然后我们遇到个小需求,有一个对象不是基于此类new出,但想要showName方法,改如何处理呢
// 这个对象只有一个name属性,此时它想拥有一个showName方法
var animal = {
name: 'cat'
}
// 虽然它可以再写一遍showName方法,可从代码设计角度来说不太优雅,可重用的方法写了两遍。
// 于是创始者发明了call、apply、bind三个方法来解决此问题
// 1 call
person.showName.call(animal);
// 2 apply
person.showName.apply(animal);
// 3 bind
person.showName.bind(animal)();
call的定义

apply的定义

我们发现apply和call差不多,差别是一个传数组,一个传多个参数。这是创始者为了开发者在不同的语境方便调用不同的方法。(就比如你设计一个类库时可能也会暴露两个不同的API,方便开发者调用)
bind的定义

call的应用
// 1.使用 call 方法调用父构造函数
// 一般用call,可以明确的看到传递的参数
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price);
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);
console.log(cheese);
console.log(fun);
apply的应用
// 1.数组合并
// 一般用apply,传递数组方便
var arr1 = [1, 2, 3];
var arr2 = [4, 5, 6];
[].push.apply(arr1, arr2);
console.log(arr1);
console.log(arr2);
// arr1 [1, 2, 3, 4, 5, 6]
// arr2 [4,5,6]
// 2.调用封装好的内置函数
/* 找出数组中最大/小的数字 */
var numbers = [5, 6, 2, 3, 7];
/* 应用(apply) Math.min/Math.max 内置函数完成 */
var max = Math.max.apply(null, numbers); /* 基本等同于 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */
var min = Math.min.apply(null, numbers);
// 如果我们不用apply,那只能用一般的for循环逐一查找
var max = -Infinity;
var min = +Infinity;
for (var i = 0; i < numbers.length; i++) {
if (numbers[i] > max) { max = numbers[i]; }
if (numbers[i] < min) { min = numbers[i]; }
}
bind的应用
// 1.配合 setTimeout 绑定this到当前实例,而不是window
function Person(name, age){
this.name = name;
this.age = age
}
Person.prototype = {
constructor: Person,
showName: function(){
console.log(this.name);
},
showAge: function(){
setTimeout(function () {
console.log(this.age)
}.bind(this), 1000)
// setTimeout(function () {
// console.log(this.age)
// }, 1000)
// 如果不bind,这里的setTimeOut是window下面的方法,所以this指向会指到window,而window下没有age,所以会输出undefined
// setTimeout( () => {
// console.log(this.age)
// }, 1000)
// 由于es6里出现的箭头函数,他能将this指向当前调用的实例上,所以用此方法也是可行的
}
}
// 用该类new出的新对象拥有此类的属性和方法
var person = new Person('wuqinhao', 26);
person.showAge();
// 一秒中后打印 26
// 2.偏函数(使一个函数拥有预设的初始参数)
function addArguments(arg1, arg2) {
return arg1 + arg2
}
var result1 = addArguments(1, 2); // 3
// 创建一个函数,它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37);
var result2 = addThirtySeven(5);
// 37 + 5 = 42
var result3 = addThirtySeven(5, 10);
// 37 + 5 = 42 ,第二个参数被忽略
/* 只要将这些参数(如果有的话)作为bind()的参数写在this后面。
当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,
传递给绑定函数的参数会跟在它们后面。*/
call模拟实现
1.模拟实现基本语法
我们看前面的引子示例,animal想执行showName方法,但又不想重新写一遍就调用了call方法。如果我们写一遍showName方法,执行完,然后再删除这个方法,而把这个过程封装成一个函数,这个函数不就有点像call嘛。
所以我们模拟的步骤可以分为:
- 1.将函数设为对象的属性
- 2.执行该函数
- 3.删除该函数
// 第一版
Function.prototype.call2 = function(context) {
// 首先要获取调用call的函数,用this可以获取
context.fn = this;
context.fn();
// fn这个名字可以随便取,因为后面会delete掉
delete context.fn;
}
// 测试一下(此代码的前提是要有引子里的代码)
person.showName.call2(animal); // cat
2.模拟实现传递参数
注意:传入的参数并不确定,我们可以从 Arguments 对象中取值,取出第二个到最后一个参数,然后放到一个数组里。
// 第二版
Function.prototype.call2 = function(context) {
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
console.log(args); // ["arguments[1]", "arguments[2]"]
eval('context.fn(' + args +')'); // 相当于eval('context.fn(arguments[1],arguments[2])')
// args会自动调用 Array.toString()
delete context.fn;
}
// 测试一下
// 1.使用 call 方法调用父构造函数
// 一般用call,可以明确的看到传递的参数
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call2(this, name, price);
this.category = 'food';
}
var cheese = new Food('feta', 5);
console.log(cheese);
eval语法请看 developer.mozilla.org/zh-CN/docs/… Arguments语法请看 developer.mozilla.org/zh-CN/docs/…
3.模拟实现this绑定null和函数返回值
1.this 参数可以传 null,当为 null 的时候,视为指向 window(当不绑定this时,可以指向window)
2.函数是可以有返回值(retrun 出eval的结果即可)
// 第三版
Function.prototype.call2 = function(context) {
context = context || window;
context.fn = this;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
console.log(args); // ["arguments[1]", "arguments[2]"]
var result = eval('context.fn(' + args +')'); // 相当于eval('context.fn(arguments[1],arguments[2])')
// args会自动调用 Array.toString()
delete context.fn;
return result;
}
// 测试一下
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
bar.call2(null); // 2
console.log(bar.call2(obj, 'kevin', 18));
// 1
// Object {
// value: 1,
// name: 'kevin',
// age: 18
// }
apply模拟实现
apply的实现与call类型,区别在与apply传入数组。所以arguments获取改成arr数组。
Function.prototype.apply2 = function(context, arr) {
context = context || window;
context.fn = this;
var result;
if (!arr) {
result = context.fn()
} else {
var args = [];
for(var i = 1, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
console.log(args); // ["arr[1]", "arr[2]"]
result = eval('context.fn(' + args +')'); // 相当于eval('context.fn(arr[1],arr[2])')
// args会自动调用 Array.toString()
}
delete context.fn;
return result;
}
bind模拟实现
1.返回一个函数。2.可以传入参数。3.一个绑定函数也能使用new操作符创建对象。
1.模拟返回函数
Function.prototype.bind2 = function (context) {
var self = this;
return function () {
// 应用apply来指定this的指向
self.apply(context);
}
}
有时绑定的函数可能也会有返回值,所以绑定的函数return了一个内容,如果不再加个return,内容是返回不出来的。
Function.prototype.bind2 = function (context) {
var self = this;
return function () {
// 应用apply来指定this的指向
return self.apply(context); // 再加一个return,将绑定函数的返回值return出来。
}
}
2.模拟传参
先看一个例子(传参还能在返回的函数里继续传)
var foo = {
value: 1
};
function bar(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
var bindFoo = bar.bind(foo, 'daisy', '18');
bindFoo()
// 1
// daisy
// 18
var bindFoo1 = bar.bind(foo, 'daisy');
bindFoo1('18');
// 1
// daisy
// 18
解决办法:还是使用arguments,只不过在最后将参数合并到一起再使用
Function.prototype.bind2 = function (context) {
var self = this;
// 获取bind2函数从第二个参数到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var bindArgs = Array.prototype.slice.call(arguments);
// 应用apply来指定this的指向
return self.apply(context, args.concat(bindArgs)); // 再加一个return,将绑定函数的返回值return出来。
}
}
3.模拟bind返回的函数作为构造函数调用
看个例子
var value = 2;
var foo = {
value: 1
};
function bar (name, age) {
this.a = 'aaa';
console.log(this.value);
console.log(name);
console.log(age);
}
bar.prototype.b = 'bbb';
var bindFoo = bar.bind(foo, 'wqh');
var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.a);
console.log(obj.b);
// shopping
// kevin
我们看到现象:当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。this指向了obj,然而obj上没有value属性,所以是undefined
实现
Function.prototype.bind2 = function (context) {
var self = this;
// 获取bind2函数从第二个参数到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this指向实例,此时结果为true,将绑定函数的this指向该实例,可以让实例获得来自绑定函数的值
return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
// 当作为普通函数时,this指向window,此时结果未false,将绑定函数的this指向context
// 以上代码如果改成`this instanceof fBound ? null : context`实例只是一个空对象,将null改为this,实例会具有绑定函数的属性
}
fBound.prototype = this.prototype;
return fBound;
}
优化一波
1.在这个写法中,我们直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以通过一个空函数来进行中转 2.当调用bind的不是函数时,我们要抛出异常
Function.prototype.bind2 = function (context) {
if (typeof this !== 'function') {
throw new Error('Function.prototype.bind - 试图绑定的内容不可调用');
}
var self = this;
// 获取bind2函数从第二个参数到最后一个参数
var args = Array.prototype.slice.call(arguments, 1);
var f = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// 当作为构造函数时,this指向实例,此时结果为true,将绑定函数的this指向该实例,可以让实例获得来自绑定函数的值
return self.apply(this instanceof f ? this : context, args.concat(bindArgs));
// 当作为普通函数时,this指向window,此时结果未false,将绑定函数的this指向context
// 以上代码如果改成`this instanceof f ? null : context`实例只是一个空对象,将null改为this,实例会具有绑定函数的属性
}
f.prototype = this.prototype;
fBound.prototype = new f();
return fBound;
}