call、apply、bind的作用是改变函数运行时this的指向。
一、this
1、普通函数中的this
function fn1(){
console.log(this) //window
}
fn1()
var a = 1;
function fn(){
console.log(this.a);
}
fn(); //输出1
setTimeout(function() {
console.log(this) //window
function fn(){
console.log(this) //window
}
fn()
}, 0);
在前面加上:'use strict'
综上:普通函数中,在非严格模式下this指向全局对象window,严格模式下禁止指向全局对象,及this为undefined。
2、对象方法中的this
var a = 1
var obj1 = {
a:2,
fn:function(){
console.log(this.a)
}
}
obj1.fn() //输出2--this指向调用函数的对象
var fn = obj1.fn;
fn() //输出1--相当于普通函数
setTimeout(obj1.fn, 0); //输出1--和上面一种一样,普通函数调用
(<object>.<function>())-》作为对象的方法调用:this指向调用者<object>。
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25, 正常结果
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空
getAge(); // NaN
var fn = xiaoming.age; // 先拿到xiaoming的age函数
fn(); // NaN
如下改版:
'use strict';
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - this.birth;
}
return getAgeFromBirth();
}
};
xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined
原因:this指针只在age方法的函数内指向xiaoming,在函数内部定义的函数,this又指向undefined了!(在非strict模式下,它重新指向全局对象window!)
修改如上写法,使其this仍然指向当前对象,是在当前对象的方法上保存this:
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var that = this; // 在方法内部一开始就捕获this
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - that.birth; // 用that而不是this
}
return getAgeFromBirth();
}
};
xiaoming.age(); // 25
改成箭头函数:
var obj = {
birth: 1990,
getAge: function () {
var b = this.birth; // 1990
var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
return fn();
}
};
obj.getAge(); // 25
//注意箭头函数还是需要通过对象调用
var func = obj.getAge
func(); //NaN
箭头函数完全修复了this的指向,this总是指向词法作用域,也就是外层调用者obj:
注意
- 由于箭头函数this的指向,所以以前的那种hack写法就不需要了:var that = this;
- 由于this在箭头函数中已经按照词法作用域绑定了,所以,用call()或者apply()调用箭头函数时,无法对this进行绑定,即传入的第一个参数被忽略:
var obj = {
birth: 1990,
getAge: function (year) {
var b = this.birth; // 1990
var fn = (y) => y - this.birth; // this.birth仍是1990
return fn.call({birth:2000}, year);
}
};
obj.getAge(2015); // 25
3、dom
4、构造函数中的this
构造函数调用模式(obj = new <function>()):this指向被构造者obj。
没有return或return普通类型,那么this指向o,所以this.a=37,就是o.a=37即给o添加新属性;return 对象,new之后得到的o就指向这个对象,所以o={a:38}
5、apply/call/bind方法:this指向方法的第一个参数。
call/apply方法中的this:动态修改函数中的this指向,第一个参数作为this的指向.
apply/call:this指向第一个参数,但是若是传null和undefined,在非严格模式下则指向全局对象,即在浏览器中指向window,严格模式下不允许指向全局对象,即为null/undefined
6、严格模式下,this不允许指向全局对象,此时this值为undefined。
总结:
- this的4种绑定规则:默认绑定、隐式绑定、显式绑定和new绑定,分别对应函数的四种调用方式:独立调用、方法调用、间接调用和构造函数调用。
- this绑定优先级:new绑定>显式绑定>隐式绑定>默认绑定
1)默认绑定:全局环境中,this默认绑定到window
- 全局环境中,this默认绑定到window
console.log(this === window);//true
- 函数独立调用时,this默认绑定到window
function foo(){
console.log(this === window);
}
foo(); //true
- 被嵌套的函数独立调用时,this默认绑定到window
//虽然test()函数被嵌套在obj.foo()函数中,但test()函数是独立调用,而不是方法调用。所以this默认绑定到window
var a = 0;
var obj = {
a : 2,
foo:function(){
function test(){
console.log(this.a);
}
test();
}
}
obj.foo();//0
- IIFE立即执行函数实际上是函数声明后直接调用执行
var a = 0;
function foo(){
(function test(){
console.log(this.a); //this指向window
})()
};
var obj = {
a : 2,
foo:foo
}
obj.foo();//0
- 闭包中内部函数的this默认指向window对象
var a = 0;
function foo(){
function test(){
console.log(this.a);
}
return test;
};
var obj = {
a : 2,
foo:foo
}
obj.foo()();//0
由于闭包的this默认绑定到window对象,但又常常需要访问嵌套函数的this,所以常常在嵌套函数中使用var that = this,然后在闭包中使用that替代this,使用作用域查找的方法来找到嵌套函数的this值
2)隐式绑定
隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。使用对象调用函数进行绑定( obj.fun() ) ,执行obj.fun(),obj就是上下文环境。注意:隐式绑定可能丢失。
function foo(){
console.log(this.a);
};
var obj1 = {
a:1,
foo:foo,
obj2:{
a:2,
foo:foo
}
}
//foo()函数的直接对象是obj1,this隐式绑定到obj1
obj1.foo();//1
//foo()函数的直接对象是obj2,this隐式绑定到obj2
obj1.obj2.foo();//2
隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到window。这种情况容易出错却又常见
var a = 0;
function foo(){
console.log(this.a);
};
var obj = {
a : 2,
foo:foo
}
//把obj.foo赋予别名bar,造成了隐式丢失,因为只是把foo()函数赋给了bar,而bar与obj对象则毫无关系
var bar = obj.foo;
bar();//0
3)显式绑定
通过call()、apply()、bind()方法把对象绑定到this上,叫做显式绑定。对于被调用的函数来说,叫做间接调用
var a = 0;
function foo(){
console.log(this.a);
}
var obj1 = {
a:1
};
var obj2 = {
a:2
};
foo.call(obj1);//1
var bar = function() {
foo.call( obj2 );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的 bar 不可能再修改它的 this
bar.call( window ); // 2
javascript中新增了许多内置函数,具有显式绑定的功能,如数组的5个迭代方法:map()、forEach()、filter()、some()、every()
var id = 'window';
function foo(el){
console.log(el,this.id);
}
var obj = {
id: 'fn'
};
[1,2,3].forEach(foo);//1 "window" 2 "window" 3 "window"
[1,2,3].forEach(foo,obj);//1 "fn" 2 "fn" 3 "fn"
注意:箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。
function foo() {
setTimeout(() => {
// 这里的 this 在此法上继承自 foo()
console.log( this.a );
},100);
}
var obj = {
a:2
};
foo.call( obj ); // 2
例外:
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
(p.foo = o.foo)(); // 2 仅仅是把o.foo传递给p.foo,并不会把o对象传入
4)new绑定
如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。对于this绑定来说,称为new绑定。理解new一个对象发生的过程。this指向创建的新对象。
function fn(){
this.a = 2;
}
var test = new fn();
console.log(test.a);//2
//优先级探索1
//1.显式绑定和隐式绑定的优先级?
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3 可以看到显式绑定将隐式绑定的this给更改了
obj2.foo.call( obj1 ); // 2 可以看到显式绑定将隐式绑定的this给更改了
//优先级探索2
//隐式绑定和new绑定的优先级?
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
obj1.foo( 2 );
console.log( obj1.a ); // 2
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4 new绑定后创建了一个新对象对this进行绑定,new的优先级更高
//优先级探索3
//显式绑定和new绑定的优先级?
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3);
console.log( obj1.a ); // 2
console.log( baz.a ); // 3 new能将this绑定到自身新建的对象上,new的优先级更高
二、区别
1、区别1
var xw = {
name : "小王",
gender : "男",
age : 24,
say : function() {
alert(this.name + " , " + this.gender + " ,今年" + this.age);
}
}
xw.say(); //小王 , 男 ,今年24
var xh = {
name : "小红",
gender : "女",
age : 18
}
修改say方法的this指向为xh:
xw.say.call(xh);
xw.say.apply(xh);
xw.say.bind(xh)();
从上面可以看出,bind与apply、call最大的区别:bind不会立即调用,其他两个会立即调用=》bind返回对应函数, 便于稍后调用; apply, call则是立即调用。
2、区别2
var obj = {
message: 'My name is: '
}
function getName(firstName, lastName) {
console.log(this.message + firstName + ' ' + lastName)
}
传参:
getName.call(obj, 'Dot', 'Dolby')
getName.apply(obj, ['Dot', 'Dolby']) // My name is: Dot Dolby
//bind
function fn(a, b, c) {
console.log(a, b, c);
}
var fn1 = fn.bind(null, 'Dot');
fn('A', 'B', 'C'); // A B C
fn1('A', 'B', 'C'); // Dot A B
fn1('B', 'C'); // Dot B C
fn.call(null, 'Dot'); // Dot undefined undefined
apply与call传参的区别:apply传参是通过数组,call传参是按顺序传入。在确定的参数下,还是最好用call,call的效果会更高,但是在函数的延展性上使用apply更好。
1)bind方法实现函数柯里化curring:
什么叫柯里化:预先设置一些参数,是指这样一个函数,它接收函数 A,并且能返回一个新的函数,这个新的函数能够处理函数 A 的剩余参数
如下案例:不想改变this,bind直接传undefined即可,传入的100,默认是给了第一个参数a,之后调用func(1,2),a已经有值了,所以b=1,c=2;第二次调用:第一次a绑定了100,第二次bind那么就把值bind到b上即b=200,那么func(10)就是c=10
2)bind与new:
如上,直接调用函数。此时函数中的this,非严格模式下默认指向的是全局对象,严格模式下不允许指向全局就是undefined。但是使用了.bind之后就会修改其中this的指向,指向传入的第一个参数,即bind之后this指向{a:1},this.b=100为该对象添加了新属性。
new 函数构造器形式,其中this:没有return或者return返回基本类型,则会作为this返回(this指向的是空对象,该空对象的原型是foo.protytype,此时就是返回this,即空对象,但是有this.b=100,即返回的就是{b:100},注意this作为返回值忽略return),若是return 对象,则会作为函数构造器new 新对象的返回值,就此时返回的是return指定的对象。
如下涉及问题:如果一个构造函数,bind了一个对象,用这个构造函数创建出的实例会继承这个对象的属性吗?为什么?
- 不会继承,因为根据 this 绑定四大规则,new 绑定的优先级高于 bind 显示绑定,通过 new 进行构造函数调用时,会创建一个新对象,这个新对象会代替 bind 的对象绑定,作为此函数的 this,并且在此函数没有返回对象的情况下,返回这个新建的对象。
bind是ES5新增的所以只有IE9+才支持,兼容旧版本浏览器:
if (!Function.prototype.bind) {
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
// this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用
return fToBind.apply(this instanceof fBound
? this
: oThis,
// 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
aArgs.concat(Array.prototype.slice.call(arguments)));
};
// 维护原型关系
if (this.prototype) {
// 当执行Function.prototype.bind()时, this为Function.prototype
// this.prototype(即Function.prototype.prototype)为undefined
fNOP.prototype = this.prototype;
}
// 下行的代码使fBound.prototype是fNOP的实例,因此
// 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
fBound.prototype = new fNOP();
return fBound;
};
}
apply,call,bind三者的区别总结:
- 三者都可以改变函数的this对象指向。
- 三者第一个参数都是this要指向的对象,如果没有这个参数或参数为undefined或null,则默认指向全局window(非严格模式下)。
- 三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入。
- bind 是返回绑定this之后的函数,便于稍后调用;apply 、call 则是立即执行 。
注意:在 ES6 的箭头函数下, call 和 apply 将失效, 对于箭头函数来说:
- 箭头函数体内的 this 对象, 就是定义时所在的对象, 而不是使用时所在的对象;所以不需要类似于
var _this = this
这种丑陋的写法 - 箭头函数不可以当作构造函数,也就是说不可以使用 new 命令, 否则会抛出一个错误
- 箭头函数不可以使用 arguments 对象,,该对象在函数体内不存在. 如果要用, 可以用 Rest 参数代替
- 不可以使用 yield 命令, 因此箭头函数不能用作 Generator 函数,什么是Generator函数可自行查阅资料,推荐阅读阮一峰Generator 函数的含义与用法,Generator 函数的异步应用
三、手写call、apply、bind
如上,后面不能用箭头函数,箭头函数没有this,是捕获上下文的this供自己使用,所以this为undefined。用普通函数,this就是调用该方法的函数对象即arr。
所以下面foo.myCall(obj, 1, 2),myCall函数中this是前面的foo函数,myCall函数的参数context和args就是后面调用括号内的参数,也可以用arguments获取。
具体如下,bind、apply和call是有返回值的:
//手写call、apply、bind
Function.prototype.myCall = function (context, ...args) {
// const args = [...arguments].slice(1) //排除第一个参数this指向,后面的参数都是执行的函数要接受的参数
context = context || window;
if (typeof this !== "function") {
throw new TypeError("Error");
}
const fnSymbol = Symbol("fn");
context[fnSymbol] = this;
// context.fn = this; //this就是调用的函数,即本例下面的foo函数
// const result = context.fn(...args);
// delete context.fn;
const result = context[fnSymbol](...args);
delete context[fnSymbol]; //恢复context,把新加入的fn再删除
return result;
};
Function.prototype.myApply = function (context, args) {
if (typeof this !== "function") {
throw new TypeError("Error");
}
context = context || window;
const fnSymbol = Symbol("fn");
context[fnSymbol] = this;
const result = context[fnSymbol](...args);
delete context[fnSymbol];
// const result = this(...args); //直接调用而不是把fn绑定到context上那么this函数中若是使用了this即获取修改this指向的obj中的属性,那么访问不了obj中的属性
return result;
};
Function.prototype.myBind = function (context, ...args) {
if (typeof this !== "function") {
throw new TypeError("Error");
}
context = context || window;
const fnSymbol = Symbol("fn");
context[fnSymbol] = this;
return function F(..._args) {
args = args.concat([...arguments]);
let result;
if (this instanceof F) {
//根据this绑定规则的优先级,new的优先级高于bind,所以此处若是new来的,this指向非之前的
result = context[fnSymbol].apply(this, args);
} else {
result = context[fnSymbol].apply(context, args);
}
delete context[fnSymbol];
return result;
};
};
function foo(a, b) {
console.log(a, b, this.age); //context.fn = this就是为了函数中使用this.age
return a + b;
}
function foo2(a, b) {
console.log(a, b, this.age); //context.fn = this就是为了函数中使用this.age
return {
age: 21,
};
}
const obj = {
age: 20,
};
// foo.myCall(obj, 1, 2);
// foo.myApply(obj, [1, 2]);
const cur = foo2.myBind(obj, 1);
// cur(2);
const o = new cur(2);
console.log(o, o.age);
上面myBind最后delete context[fnSymbol];有问题,案例bind后只执行了一次正好,再次执行cur(3),此时函数已经被删除了context[fnSymbol]再执行会报做错
报错如下:
修改如下:里面使用了apply已经进行了this指向的绑定,所以不需要之前的操作,下面是维护原型关系:
Function.prototype.myBind = function (context) {
if (typeof this !== "function") {
throw new TypeError("Error");
}
const fToBind = this;
const aArgs = Array.prototype.slice.call(arguments, 1);
function F() {
const args = aArgs.concat(Array.prototype.slice.call(arguments));
let result;
if (this instanceof F) {
//根据this绑定规则的优先级,new的优先级高于bind,所以此处若是new来的,this指向非之前的
result = fToBind.apply(this, args);
} else {
result = fToBind.apply(context, args);
}
// delete context[fnSymbol]; //此处删除,F只能执行一次,第二次后就已context[fnSymbol]就没有了执行会报错
return result;
}
function fNOP() {}
if (this.prototype) {
fNOP.prototype = this.prototype;
}
F.prototype = new fNOP();
return F;
};
注意:context.fn = this的作用