this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计得更加简洁并且易于复用。
function identify(){
return this.name.toUpperCase();
}
function speak(){
var greeting = "Hello,I'm "+identify.call(this);
console.log(greeting);
}
var me = {
name:"Kyle"
};
var you = {
name:"Reader"
};
identify.call(me);//KYLE
identify.call(you);//READER
speak.call(me);//Hello,I'm KYLE
speak.call(you);//Hello,I'm READER
对this的误解
- 把this理解为指向函数自身
function foo(num){
console.log("foo:"+num);
//记录foo被调用的次数
this.count++;
}
foo.count = 0;
var i;
for(i=0;i<10;i++){
if(i>5){
foo(i);
}
}
//foo:6
//foo:7
//foo:8
//foo:9
console.log(foo.count);//0
执行foo.count =0时,的确向函数对象foo添加了一个属性count。但是函数内部代码this.count中的this并不是指向那个函数对象。实际上,创建了一个全局变量count。
function foo(num){
console.log("foo:"+num);
//记录foo被调用的次数
this.count++;
}
foo.count = 0;
var i;
for(i=0;i<10;i++){
if(i>5){
//使用call()可以确保this指向函数对象foo本身
foo.call(foo,i);
}
}
//foo:6
//foo:7
//foo:8
//foo:9
console.log(foo.count);//4
- 认为this指向函数的作用域
这个问题有点复杂,因为在某种情况下它是正确的,但是在其他情况下它却是错误的。
需要明确的是, this 在任何情况下都不指向函数的词法作用域。在 JavaScript 内部,作用域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript代码访问,它存在于 JavaScript 引擎内部。
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log( this.a );
}
foo(); // ReferenceError: a is not defined
首先,这段代码试图通过 this.bar() 来引用 bar() 函数。这是绝对不可能成功的,我们之后会解释原因。调用 bar() 最自然的方法是省略前面的 this,直接使用词法引用标识符。此外,编写这段代码的开发者还试图使用 this 联通 foo() 和 bar() 的词法作用域,从而让bar() 可以访问 foo() 作用域里的变量 a。这是不可能实现的,你不能使用 this 来引用一个词法作用域内部的东西。
每当你想要把 this 和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的。
this到底是什么
this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。 this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。 this 就是记录的其中一个属性,会在函数执行的过程中用到。
this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
this全面解析
调用位置
寻找调用位置一般是寻找“函数被调用的位置”,但是有时候某些地方会隐藏真正的调用位置。最重要的是分析调用栈。
function baz(){
//当前的调用栈是:baz
//因此当前调用位置是全局作用域
console.log("baz");
bar();//<-- bar的调用位置
}
function bar(){
//当前调用栈是baz->bar
//当前调用位置在baz中
console.log("bar");
foo();//<-- foo的调用位置
}
function foo(){
//当前调用栈是baz->bar->foo
//当前调用位置在bar中
console.log("foo");
}
baz();//<-- baz的调用位置
绑定规则
默认绑定
//独立函数调用
function foo(){
console.log(this.a);
}
var a = 2;
/*foo()是直接使用不带任何修饰符的函数引用进行调用的,
因此只能使用默认绑定,无法应用其他规则,this指向全局对象*/
foo();//2 this.a被解析成全局变量a
如果使用严格模式(strict mode),那么全局对象将无法使用默认绑定,因此 this 会绑定到 undefined
function foo(){
"use strict";
console.log(this.a);
}
var a = 2;
foo();//TypeError: this is undefined
这里有一个微妙但是非常重要的细节,虽然 this 的绑定规则完全取决于调用位置,但是只有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象;严格模式下与 foo()的调用位置无关
function foo(){
console.log(this.a);
}
var a = 2;
(function(){
"use strict";
foo();//2
})();
隐式绑定
function foo(){
console.log(this.a);
}
/*无论是直接在 obj 中定义还是先定义再添加为引用属性,
这个函数严格来说都不属于obj 对象。然而,调用位置会使
用 obj 上下文来引用函数,因此可以说函数被调用时 obj
对象“拥有”或者“包含”它。
*/
var obj = {
a:2,
foo:foo
};
obj.foo();//2
当 foo() 被调用时,它的落脚点确实指向 obj 对象。当函数引用有上下文对象时, 隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。因为调用 foo() 时 this 被绑定到 obj,因此 this.a 和 obj.a 是一样的。
对象属性引用链中只有顶层或者说最后一层会影响地哦啊用位置。
function foo(){
console.log(this.a);
}
var obj2 = {
a:42,
foo:foo
};
var obj1 = {
a:2,
obj2:obj2
}
obj1.obj2.foo();//42
隐式丢失:被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式。
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
var bar = obj.foo;//只是引用函数本身
var a = "oops,global";
/*bar()实质是一个不带任何修饰的函数调用,
应用了默认绑定,相当于直接调用
foo();
*/
bar();//"oops,global"
显式绑定
在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。
JavaScript 中的“所有”函数都有一些有用的特性,可以用来解决这个问题。具体点说,可以使用函数的 call(..) 和apply(..) 方法。严格来说, JavaScript 的宿主环境有时会提供一些非常特殊的函数,它们并没有这两个方法。但是这样的函数非常罕见, JavaScript 提供的绝大多数函数以及自己创建的所有函数都可以使用 call(..) 和 apply(..) 方法。
它们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调用函数时指定这个 this。因为你可以直接指定 this 的绑定对象,因此称之为显式绑定。
function foo(){
console.log(this.a);
}
var obj = {
a:2
};
foo.call(obj);//2
如果你传入了一个原始值(字符串类型、布尔类型或者数字类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式。这通常被称为“装箱”。
硬绑定
function foo(){
console.log(this.a);
}
var obj = {
a:2
};
var bar = function(){
foo.call(obj);//手动调用,强制将foo的this绑定到obj
};
bar();//2
setTimeout(bar,100);//2
//硬绑定的bar不可能再修改它的this
bar.call(window);//2
硬绑定应用场景——创建一个包裹函数,传入所有的参数并返回接收到的所有值:
function foo(something){
console.log(this.a,something);
return this.a+something;
}
var obj = {
a:2
};
var bar = function(){
return foo.apply(obj,arguments);
};
var b = bar(3);//2 3
console.log(b);//5
创建一个i可以重复使用的辅助函数
function foo(something){
console.log(this.a,something);
return this.a+something;
}
//简单的辅助绑定函数
function bind(fn,obj){
return function(){
return fn.apply(obj,arguments);
};
}
var obj = {
a:2
};
var bar = bind(foo,obj);
var b = bar(3);//2 3
console.log(b);//5
由于硬绑定是一种非常常用的模式,所以在 ES5 中提供了内置的方法 Function.prototype.bind
function foo(something){
console.log(this.a,something);
return this.a + something;
}
var obj = {
a:2
};
/*bind()会返回一个硬编码的新函数,它会把参数设置为
this的上下文并调用原始函数*/
var bar = foo.bind(obj);
var b = bar(3);//2 3
console.log(b);//5
API调用的“上下文”
第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保回调函数使用指定的 this。
function foo(el){
console.log(el,this.id);
}
var obj = {
id:"awesome"
};
//调用foo()时把this绑定到obj
[1,2,3].forEach(foo,obj);
//1 awesome 2 awesome 3 awesome
new绑定
在 JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上,它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。
使用new来调用函数,或者发生构造函数调用时,会自动执行:
- 创建一个全新的对象
- 这个对象会被执行[[原型]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a){
this.a = a;
}
/*使用 new 来调用 foo(..) 时,我们会构造
一个新对象并把它绑定到 foo(..) 调用中的
this上。*/
var bar = new foo(2);
console.log(bar.a);//2
优先级
隐式绑定和显式绑定
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 显式绑定优先级更高
obj2.foo().call(obj1);//2
new绑定和隐式绑定
function foo(something){
this.a = something;
}
var obj1 = {
foo:foo
};
var obj2 = {};
obj1.foo(2);
console.log(obj1.a);//2
obj1.foo.call(obj2,3);
console.log(obj2.a);//3
var bar = new obj1.foo(4);
console.log(obj1.a);//2
console.log(bar.a);//4 new绑定比隐式绑定优先级高
new绑定和显式绑定
function foo(something){
this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a);//2 bar被硬绑定到obj1上
/*new 修改了硬绑定(到 obj1 的)调用 bar(..) 中的 this。
因为使用了new 绑定,得到了一个名字为 baz 的新对象,并且
baz.a 的值是 3。*/
var baz = new bar(3);
console.log(obj1.a);//2
console.log(baz.a);//3
判断this:
- 函数是否在new中调用(newbd)?如果是,this绑定的是新创建的对象 var bar= new foo()
- 函数是否通过call,apply(显式绑定)或者硬绑定调用?如果是,this绑定的是指定的对象 var bar = foo.call(obj2)
- 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。 var bar = obj1.foo()
- 如果都不是的话,使用默认绑定。严格模式下绑定到undefined,否则绑定到全局对象。var bar = foo()
绑定例外
如果把null或者undefined作为this的绑定对象传入call,apply或者bind,这些值会在调用时被忽略,实际应用的是默认绑定的原则
function foo(){
console.log(this.a);
}
var a = 2;
foo.call(null);//2
更安全的this
一种“更安全”的做法是传入一个特殊的对象,把 this 绑定到这个对象不会对程序产生任何副作用。就像网络(以及军队)一样,可以创建一个“DMZ”(demilitarizedzone, 非军事区)对象——它就是一个空的非委托的对象。
function foo(a,b){
console.log("a:"+a+",b:"+b);
}
//DMZ空对象
var ∅ = Object.create(null);
//把数组展开成参数
foo.apply(∅,[2,3]);//a:2,b:3
//使用bind()进行柯里化
var bar = foo.bind(∅,2);
bar(3);//a:2,b:3
间接引用
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 对目标函数的引用,使用默认绑定
箭头函数
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this。
function foo(){
//返回一个箭头函数
return (a)=>{
//this继承自foo()
console.log(this.a);
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call(obj1);
bar.call(obj2);//2 箭头函数的绑定无法被修改
箭头函数最常用于回调函数中,例如事件处理器或者定时器:
function foo(){
setTimeout(()=>{
//这里的this在此法上继承自foo()
console.log(this.a);
},100);
}
var obj = {
a:2
};
foo.call(obj);//2