this 与 call、apply、bind 方法
一、前置知识
1、对 this 的理解
- this是执行上下文中的一个属性。
- 它指向最后一次调用这个方法的对象。
实际开发过程中,this的指向可以通过4中调用模式来判断:
1、函数调用模式
当一个函数不是某个对象的属性时,直接作为函数来调用时,this指向全局对象。
2、方法调用模式
如果一个函数作为一个对象的方法来调用时,this指向这个新创建的对象。
3、构造器调用模式
如果一个函数用new调用时,函数执行前会新创建一个对象,this指向这个新创建的对象。
4、apply、bind、call调用模式
这三个方法都是可以显示的指定调用函数的this指向。
apply方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。
call方法接收的参数,第一个是 this 的绑定的对象,后面剩余参数是传入函数指向的参数。
也就是说,在调用 call() 方法时,传递给函数的参数biu逐个列举出来。
bind方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this指向除了使用 new 时会被改变,其他情况都不会改变。
2、函数 this 的绑定规则
1、默认绑定
当函数调用类型为独立函数调用时,函数 this 为默认绑定,指向全局变量;在严格模式下,this 绑定到 undefined。
function foo() {
console.log(this.a);
}
foo(); //等价于window.foo()
2、隐式绑定
当函数的调用位置有上下文对象时,或者说函数在被调用时被某个对象拥有或者包含时,隐式绑定规则就会把函数调用中的 this 绑定到这个上下文对象。
如下:foo 在调用时 this 便被隐式绑定到了 obj 上了
function foo2() {
console.log(this.a);
}
const obj = {
a: 2,
foo,
};
obj.foo();
3、显示绑定
使用 call、apply、bind 显示的绑定函数调用时的 this 指向。注意无论是 call、bind、apply 方法,都是被另外一个方法调用
function showName(arg1, arg2) {
console.log(arg1, arg2, this.name);
}
const user = {
name: "jack",
};
showName.call(user, "hello", "world");
4、new
当使用 new 调用函数时,会发生 this 的指向绑定,但此时发生的 this 绑定与函数本身无关。
二、call、apply、bind介绍
1、call()、apply()、bind()介绍
1、call()方法 [ Function.prototype.call() ]
语法
function call(thisArg, arg1, arg2, ...)
参数
thisArg
可选的。在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
arg1, arg2, ...
指定的参数列表。
返回值
使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。
描述
call() 允许为不同的对象分配和调用属于一个对象的函数/方法。
call() 提供新的 this 值给当前调用的函数/方法。你可以使用 call 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。
案例demo
<script>
function sayHello(arg1, arg2) {
console.log(
`我是${this.name}, 今年${this.age}岁.接受传过来的值arg1: ${arg1},arg2: ${arg2}`
);
}
let user = {
name: "jack",
age: 20,
};
sayHello.call(user, "参数1", "参数2");
</script>
使用场景
1、使用 call 方法调用父构造函数
在一个子构造函数中,你可以通过调用父构造函数的 call 方法来实现继承,类似于 Java 中的写法。下例中,使用 Food 和 Toy 构造函数创建的对象实例都会拥有在 Product 构造函数中添加的 name 属性和 price 属性,但 category 属性是在各自的构造函数中定义的。
<script>
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";
}
let cheese = new Food("feta", 5);
let fun = new Toy("feta", 5);
console.log("cheese", cheese);
console.log("fun", fun);
</script>
2、使用 call 方法调用匿名函数
在下例中的 for 循环体内,我们创建了一个匿名函数,然后通过调用该函数的 call 方法,将每个数组元素作为指定的 this 值执行了那个匿名函数。这个匿名函数的主要目的是给每个数组元素对象添加一个 print 方法,这个 print 方法可以打印出各元素在数组中的正确索引号。当然,这里不是必须得让数组元素作为 this 值传入那个匿名函数(普通参数就可以),目的是为了演示 call 的用法。
<script>
let animal = [
{ species: "Lion", name: "King" },
{ species: "Whale", name: "Fail" },
];
for (let i = 0; i < animal.length; i++) {
(function (i) {
this.print = function () {
console.log("#" + i + " " + this.species + ": " + this.name);
};
this.print();
}.call(animal[i], i));
}
</script>
3、使用 call 方法调用函数并指定上下文的 'this'
在下面的例子中,当调用 greet 方法的时候,该方法的this值会绑定到 obj 对象。
<script>
function greet() {
let reply = [
this.animal,
"typically sleep between",
this.sleepDuration,
].join(" ");
console.log(reply);
}
var obj = {
animal: "cats",
sleepDuration: "12 and 16 hours",
};
greet.call(obj); // cats typically sleep between 12 and 16 hours
</script>
4、使用 call 方法调用函数并且不指定第一个参数(argument)
在下面的例子中,我们调用了 display 方法,但并没有传递它的第一个参数。如果没有传递第一个参数,this 的值将会被绑定为全局对象。
var sData = 'Wisen';
function display() {
console.log('sData value is %s ', this.sData);
}
display.call(); // sData value is Wisen
备注: 在严格模式下,
this的值将会是undefined.如下:
'use strict'
var sData = 'Wisen';
function display() {
console.log(this.sData);
}
display.call(); // Cannot read the property of 'sData' of undefined
2、apply()方法 [ Function.prototype.apply() ]
语法
apply(thisArg)
apply(thisArg, argsArray)
参数
thisArg
在 func 函数运行时使用的 this 值。请注意,this 可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
argsArray 可选
一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为 null 或 undefined,则表示不需要传入任何参数。从 ECMAScript 5 开始可以使用类数组对象。
返回值
调用有指定 this 值和参数的函数的结果。
描述
备注: 虽然这个函数的语法与
call()几乎相同,但根本区别在于,call()接受一个参数列表,而apply()接受一个参数的单数组。
在调用一个存在的函数时,你可以为其指定一个 this 对象。this 指当前对象,也就是正在调用这个函数的对象。使用 apply,你可以只写一次这个方法然后在另一个对象中继承它,而不用在新对象中重复写该方法。
apply 与 call() 非常相似,不同之处在于提供参数的方式。apply 使用参数数组而不是一组参数列表。apply 可以使用数组字面量(array literal),如 fun.apply(this, ['eat', 'bananas']),或数组对象,如 fun.apply(this, new Array('eat', 'bananas'))。
你也可以使用 arguments 对象作为 argsArray 参数。arguments 是一个函数的局部变量。它可以被用作被调用对象的所有未指定的参数。这样,你在使用 apply 函数的时候就不需要知道被调用对象的所有参数。你可以使用 arguments 来把所有的参数传递给被调用对象。被调用对象接下来就负责处理这些参数。
从 ECMAScript 第 5 版开始,可以使用任何种类的类数组对象,就是说只要有一个 length 属性和 (0..length-1) 范围的整数属性。例如现在可以使用 NodeList 或一个自己定义的类似 {'length': 2, '0': 'eat', '1': 'bananas'} 形式的对象。
备注: 许多较旧的浏览器,包括 Chrome <17 以及 Internet Explorer <9 不接受类数组对象。如果传入类数组对象,它们会抛出异常。
案例demo
<script>
function sayHello(arg1, arg2) {
console.log(
`我是${this.name}, 今年${this.age}岁.接受传过来的值arg1: ${arg1},arg2: ${arg2}`
);
}
let user = {
name: "jack",
age: 20,
};
sayHello.apply(user, ["参数1", "参数2"]); // 我是jack, 今年20岁.接受传过来的值arg1: 参数1,arg2: 参数2
</script>
使用场景
1、使用apply将数组各项添加到另一个数组
我们可以使用 push 将元素追加到数组中。由于 push 接受可变数量的参数,所以也可以一次追加多个元素。
但是,如果 push 的参数是数组,它会将该数组作为单个元素添加,而不是将这个数组内的每个元素添加进去,因此我们最终会得到一个数组内的数组。如果不想这样呢?concat 符合我们的需求,但它并不是将元素添加到现有数组,而是创建并返回一个新数组。然而我们需要将元素追加到现有数组......那么怎么做好?难道要写一个循环吗?别当然不是!
<script>
const arr = ["a", "b"];
const elements = [1, 2, 3];
// 如果使用concat会创建一个新数组
// let result = arr.concat(elements);
// console.log(result);
// 如果使用apply则在原数组上添加元素
arr.push.apply(arr, elements);
console.log(arr);
</script>
2、使用apply和内置函数
对于一些需要写循环以遍历数组各项的需求,我们可以用 apply 完成以避免循环。
下面是示例,我们将用 Math.max/Math.min 求得数组中的最大/小值。
<script>
const numbers = [-99, 5, 99, 2, 3, 7];
// 使用 Math.min/Math.max 以及 apply 函数时的代码
let maxValue = Math.max.apply(null, numbers);
let minValue = Math.min.apply(null, numbers);
// 最大值是:99,最小值是:-99
console.log(`最大值是:${maxValue},最小值是:${minValue}`);
// 对比:简单循环算法
max = Infinity;
min = -Infinity;
for (let i of numbers) {
if (i > max) {
max = i;
}
if (i < min) {
min = i;
}
}
// 最大值是:99,最小值是:-99
console.log(`最大值是:${maxValue},最小值是:${minValue}`);
</script>
注意:如果按上面方式调用 apply,有超出 JavaScript 引擎参数长度上限的风险。一个方法传入过多参数(比如一万个)时的后果在不同 JavaScript 引擎中表现不同。(JavaScriptCore 引擎中有被硬编码的参数个数上限:65536)。
这是因为此限制(实际上也是任何用到超大栈空间的行为的自然表现)是不明确的。一些引擎会抛出异常,更糟糕的是其他引擎会直接限制传入到方法的参数个数,导致参数丢失。比如:假设某个引擎的方法参数上限为 4(实际上限当然要高得多),这种情况下,上面的代码执行后,真正被传递到 apply的参数为 5, 6, 2, 3 ,而不是完整的数组。
如果你的参数数组可能非常大,那么推荐使用下面这种混合策略:将数组切块后循环传入目标方法:
<script>
function minOfArray(arr) {
let min = Infinity;
let QUANTUM = 32768;
for (let i = 0, len = arr.length; i < len; i += QUANTUM) {
const submin = Math.min.apply(
null,
arr.slice(i, Math.min(i + QUANTUM, len))
);
min = Math.min(submin, min);
}
return min;
}
let min = minOfArray([99, 6, -99, 3, 7]);
console.log(min);
</script>
3、使用apply来链接构造器
你可以使用 apply 来链接一个对象构造器,类似于 Java。在接下来的例子中我们会创建一个全局 Global_Objects/Function 对象的 construct 方法,来使你能够在构造器中使用一个类数组对象而非参数列表。
<script>
Function.prototype.construct = function (aArgs) {
let oNew = Object.create(this.prototype);
this.apply(oNew, aArgs);
return oNew;
};
function MyConstructor() {
for (let nProp = 0; nProp < arguments.length; nProp++) {
this["property" + nProp] = arguments[nProp];
}
}
let myArray = [4, "Hello World!", false];
let myConstructor = MyConstructor.construct(myArray);
console.log(myConstructor.property1); // 'Hello world!'
console.log(myConstructor instanceof MyConstructor); // 'true'
console.log(myConstructor.constructor); // 'MyConstructor'
</script>
备注: 这个非原生的
Function.construct方法无法和一些原生构造器(例如Date)一起使用。在这种情况下你必须使用Function.prototype.bind方法。例如,想象有如下一个数组要用在 Date 构造器中:[2012, 11, 4];这时你需要这样写:new (Function.prototype.bind.apply(Date, [null].concat([2012, 11, 4])))()——无论如何这不是最好的实现方式并且也许不该用在任何生产环境中。
3、bind()方法 [ Function.prototype.bind() ]
语法
function.bind(thisArg[, arg1[, arg2[, ...]]])
参数
thisArg
调用绑定函数时作为 this 参数传递给目标函数的值。如果使用new运算符构造绑定函数,则忽略该值。当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。
arg1, arg2, ...
当目标函数被调用时,被预置入绑定函数的参数列表中的参数。
返回值
返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。
描述
bind() 函数会创建一个新的绑定函数(bound function,BF)。绑定函数是一个 exotic function object(怪异函数对象,ECMAScript 2015 中的术语),它包装了原函数对象。调用绑定函数通常会导致执行包装函数。 绑定函数具有以下内部属性:
- [[BoundTargetFunction]] - 包装的函数对象
- [[BoundThis]] - 在调用包装函数时始终作为 this 值传递的值。
- [[BoundArguments]] - 列表,在对包装函数做任何调用都会优先用列表元素填充参数列表。
- [[Call]] - 执行与此对象关联的代码。通过函数调用表达式调用。内部方法的参数是一个this值和一个包含通过调用表达式传递给函数的参数的列表。
当调用绑定函数时,它调用 [[BoundTargetFunction]] 上的内部方法 [[Call]] ,就像这样 Call(boundThis, args) 。其中,boundThis 是 [[BoundThis]] ,args 是 [[BoundArguments]] 加上通过函数调用传入的参数列表。
绑定函数也可以使用 new 运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数。
案例demo
<script>
function sayHello(arg1, arg2) {
console.log(
`我是${this.name}, 今年${this.age}岁.接受传过来的值arg1: ${arg1},arg2: ${arg2}`
);
}
let user = {
name: "jack",
age: 20,
};
let fn = sayHello.bind(user, "参数1", "参数2");
fn();
</script>
使用场景
1、创建绑定函数
bind() 最简单的用法是创建一个函数,不论怎么调用,这个函数都有同样的 this 值。JavaScript 新手经常犯的一个错误是将一个方法从对象中拿出来,然后再调用,期望方法中的 this 是原来的对象(比如在回调中传入这个方法)。如果不做特殊处理的话,一般会丢失原来的对象。基于这个函数,用原始的对象创建一个绑定函数,巧妙地解决了这个问题:
<script>
this.x = 9;
var module = {
x: 81,
getX() {
return this.x;
},
};
module.getX(); // 81
var retrieveX = module.getX;
retrieveX(); // 返回 9。因为这句话等价于 window.retrieveX(),this的指向取决于最后被调用的对象
var boundGetX = retrieveX.bind(module);
boundGetX(); // 虽然等价于window.retrieveX.bind(module); 但是此时的this指向被bind方法改变成了 module.结果成81
</script>
2、偏函数
bind() 的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为 bind() 的参数写在 this 后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。
<script>
function list() {
return Array.prototype.slice.call(arguments);
}
function addArguments(arg1, arg2) {
return arg1 + arg2;
}
let listRes = list(1, 2, 3); //[1,2,3]
let result = addArguments(1, 2); // 3
console.log(listRes, result);
// 创建一个函数 它拥有预设参数列表
var leadingThirtysevenList = list.bind(null, 37); // 切记 bind返回的是一个方法
// console.log("leadingThirtysevenList:", leadingThirtysevenList()); // [37]
// 创建一个函数,它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37);
// console.log("addThirtySeven", addThirtySeven()); // NaN
var list2 = leadingThirtysevenList(); // [37]
var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
var result2 = addThirtySeven(5); // 37 + 5 = 42
var result3 = addThirtySeven(5, 10);
// 37 + 5 = 42,第二个参数被忽略
</script>
3、配合setTimeOut
在默认情况下,使用 window.setTimeout() 时,this 关键字会指向 window(或 global)对象。当类的方法中需要 this 指向类的实例时,你可能需要显式地把 this 绑定到回调函数,就不会丢失该实例的引用。
function LateBloomer() {
this.petalCount = Math.ceil(Math.random() * 12) + 1;
}
// 在 1 秒钟后声明 bloom
LateBloomer.prototype.bloom = function() {
window.setTimeout(this.declare.bind(this), 1000);
};
LateBloomer.prototype.declare = function() {
console.log('I am a beautiful flower with ' +
this.petalCount + ' petals!');
};
var flower = new LateBloomer();
flower.bloom(); // 一秒钟后,调用 'declare' 方法
4、作为构造函数使用的绑定函数
警告: 这部分演示了 JavaScript 的能力并且记录了
bind()的超前用法。以下展示的方法并不是最佳的解决方案,且可能不应该用在任何生产环境中。
绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
return this.x + ',' + this.y;
};
var p = new Point(1, 2);
p.toString(); // '1,2'
var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0/*x*/);
// 本页下方的 polyfill 不支持运行这行代码,
// 但使用原生的 bind 方法运行是没问题的:
var YAxisPoint = Point.bind(null, 0/*x*/);
/*(译注:polyfill 的 bind 方法中,如果把 bind 的第一个参数加上,
即对新绑定的 this 执行 Object(this),包装为对象,
因为 Object(null) 是 {},所以也可以支持)*/
var axisPoint = new YAxisPoint(5);
axisPoint.toString(); // '0,5'
axisPoint instanceof Point; // true
axisPoint instanceof YAxisPoint; // true
new YAxisPoint(17, 42) instanceof Point; // true
请注意,你不需要做特别的处理就可以创建一个和 new 操作符一起使用的绑定函数。也就是说,你不需要做特别处理就可以创建一个可以被直接调用的绑定函数,即使你更希望绑定函数是用 new 操作符来调用。
// ...接着上面的代码继续的话,
// 这个例子可以直接在你的 JavaScript 控制台运行
// 仍然能作为一个普通函数来调用
//(即使通常来说这个不是被期望发生的)
YAxisPoint(13);
emptyObj.x + ',' + emptyObj.y; // '0,13'
如果你希望一个绑定函数要么只能用 new 操作符,要么只能直接调用,那你必须在目标函数上显式规定这个限制。
5、快捷调用
在你想要为一个需要特定的 this 值的函数创建一个捷径(shortcut)的时候,bind() 也很好用。
你可以用 Array.prototype.slice 来将一个类似于数组的对象(array-like object)转换成一个真正的数组,就拿它来举例子吧。你可以简单地这样写:
var slice = Array.prototype.slice;
// ...
slice.apply(arguments);
用 bind()可以使这个过程变得简单。在下面这段代码里面,slice 是 Function.prototype 的 apply() 方法的绑定函数,并且将 Array.prototype 的 slice() 方法作为 this 的值。这意味着我们压根儿用不着上面那个 apply()调用了。
// 与前一段代码的 "slice" 效果相同
var unboundSlice = Array.prototype.slice;
var slice = Function.prototype.apply.bind(unboundSlice);
// ...
slice(arguments);
2、call() 和 apply() 的区别
它们的作用一模一样,区别仅在于传入参数的形式不同。
- apply 接受两个参数,第一个参数制定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
- call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。
三、实现call
1、call方法
call的实现步骤:
1、判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用call等方式调用的情况
2、判断传入上下文是否存在,如果不存在,则设置为window
3、处理传入的参数,截取第一个参数后的所有参数
4、将函数作为上下文对象的一个属性
5、使用上下文对象来调用这个对象,并保存返回结果
6、删除刚才新增的属性
7、返回结果
<script>
Function.prototype.myCall = function (context) {
// 判断调用对象
if (typeof this != "function") {
new Error("type error");
}
// 获取参数
let args = [...arguments].slice(1);
result = null;
// 判断context是否传入,如果未传入则设置为window
context = context || window;
// 将调用函数设为对象的方法
context.fn = this;
// 调用函数
result = context.fn(...args);
// 将属性删除
delete context.fn;
return result;
};
</script>
<script>
Function.prototype.myCall2 = function (context, ...args) {
// 判断传入对象是否存在,因为是可选参数
if (!context || context == null) {
context = window;
}
// 创造唯一的key值 作为我们构造的context内部方法名
let fn = Symbol();
context[fn] = this;
return context[fn](...args);
};
</script>
<script>
Function.prototype.myCall = function () {
let args = Array.prototype.slice.call(arguments);
let context = args.shift(); // 传入对象(弹出第一个参数)
context.fn = this;
console.log(args);
let result = context.fn(...args);
delete context.fn;
return result;
};
function fn(a, b) {
console.log(this.name, a, b);
}
fn.myCall({ name: "jack" }, 20, 30);
</script>
2、apply方法
apply函数的实现步骤:
1、判断调用对象是否为函数,即使是定义在函数的原型上,但是可能出现使用call方法的情况
2、判断传入上下文对象是否存在,如果存在,则设置为window
3、将函数作为上下文对象的一个属性
4、判断参数值是否传入
5、使用上下文对象来调用这个方法,并保存返回结果
6、删除刚才新增的属性
7、返回结果
<script>
Function.prototype.myApply = function (context) {
if (typeof this !== "function") {
throw new TypeError("Type error");
}
if (!context || context == null) {
context = window;
}
let result = null;
// 将函数设为对象的方法
context.fn = this;
// 调用方法
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
// 删除方法
delete context.fn;
return result;
};
</script>
<script>
Function.prototype.myApply = function () {
let args = Array.prototype.slice.call(arguments);
let context = args.shift();
context.fn = this;
let result = context.fn();
delete context.fn;
return result;
};
function fun(a, b) {
console.log(a, b, this.name);
}
let user = {
name: "jack",
};
fun.myApply(user, [20, 30]);
</script>
3、bind方法
bind函数的实现步骤:
1、判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。
2、保存当前函数的引用,获取其余传入参数
3、创建一个函数返回
4、函数内部使用apply来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的this给apply调用,其余情况都传入指定的上下文对象
<script>
Function.prototype.myBind = function (context) {
if (typeof this !== "function") {
throw new TypeError("Type Error");
}
if (!context || context == null) {
context = window;
}
let args = [...arguments].slice(1);
fn = this;
return function Fn() {
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
);
};
};
function sayHello(name, age) {
console.log(this.name, this.age);
}
let user = {
name: "jack",
age: 20,
};
let res = sayHello.myBind(user, "rose", 30);
res();
</script>