学习了这么久的javascript,以前一直以为this指的就是指调用该函数或方法的对象。但是这种解释在有些情况下并不合理。通过阅读相关书籍今天算是彻底弄懂了this具体指的是啥,便将此记录下来,供自己以后复习。
1、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 的调用位置
2、this的绑定方式
this的绑定方式主要分为以下4种。2.1 默认绑定
函数独立调用,非严格模式this 指向全局对象.严格模式下,this指向undefined。 详情请看如下代码: var a=2;
function test(){
console.log(this.a);//this指的是window
function foo(){
console.log(this.a);//this指的是window
}
foo();//2
}
test();//2
var a=2;
function test(){
"use strict"
console.log(this.a);//this指的是undefined;
function foo(){
console.log(this.a);
}
foo();
}
test();
2.2 隐式绑定
该绑定规则说的是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。看下面的例子: function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
调用obj.foo()时,调用foo()时使用的是obj的上下文对象,所以在foo()函数中的this绑定着obj。
对象属性引用链中只有最顶层或者说最后一层会影响调用位置。请看下面的例子。
function foo() {
console.log( this.a );
}
var obj3={
a:32,
foo:foo
}
var obj2 = {
a: 42,
obj3: obj3
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.obj3.foo(); // 32
隐式绑定容易发生隐式丢失的问题,也就是被隐式绑定的函数会丢失绑定对象。也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上(取决于是否是严格模式)。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
//就相当于var bar=foo;
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global
虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定。在看下面的例子:
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"
参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。再看下面的一个例子:
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
setTimeout( obj.foo, 100 ); // "oops, global"
把函数传入语言内置的函数而不是传入你自己声明的函数,其结果跟上面的结果是一样的。
2.3 显示绑定
使用函数的 call(..) 和apply(..) 方法,来将第一个参数显示的绑定到this function foo() {
console.log( this.a );
}
var obj = {
a:2
};
foo.call( obj ); // 2
function foo() {
console.log( this.a );
}
var obj2={
a:23,
}
var obj1 = {
a:2,
obj2:obj2
};
foo.call( obj1.obj2 ); // 23
这说明显式绑定仍然无法解决我们之前提出的丢失绑定问题。
2.3.1 硬绑定
显示绑定也会出现绑定丢失的问题,所以这里提出了一种叫做硬绑定的绑定方法。这种方法其实就是在函数内部调用call()或者apply()方法。
function foo() {
console.log( this.a );
}
var obj = {
a:2
};
var bar = function() {
foo.call( 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
ES5 中提供了内置的方法 Function.prototype. bind,用于硬绑定。
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a:2
};
var bar = foo.bind( obj );
//这句代码其实就是将obj绑定到foo()函数中的this,并将对foo()函数的引用赋值给bar。
var b = bar( 3 ); // 2 3
console.log( b ); // 5
2.3.2 原生API
javascript还有一些原生API的一些内置函数,都提供了一
个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保你的回调函数使用指定的 this。 这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定,
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
};
// 调用 foo(..) 时把 this 绑定到 obj
[1, 2, 3].forEach( foo, obj );//将obj绑定到foo()函数的this
// 1 awesome 2 awesome 3 awesome
2.4 new绑定
使用 new 初始化类 调用类中的构造函数。具体详情可以参考这片文章 javascript中初始化构造函数时new所起的作用。
这四种绑定方式的优先级如下:
1、默认绑定肯定是这四种绑定方式中优先级最低的;
2、显式绑定比隐式绑定优先级高
function foo(){
console.log(this.a);
}
var obj1={
a:2
};
var obj2={
a:3
}
var a=1;
obj1.foo();//2
obj2.foo();//3
obj1.foo.call(obj2);//3;
obj2.foo.call(obj1);//2
3、new绑定比隐式绑定优先级高
function foo(arg){
this.a=arg;
}
var obj1={
foo:foo
};
var obj2={};
obj1.foo(2);
console.log(obj1.a);//2
obj1.foo.call(obj3,3);//3;
var bar=new obj.foo(4);
console.log(obj1.a);//2
console.log(bar.a);//4
4、new绑定比显式绑定中的(硬绑定)优先级高。
function foo(arg){
this.a=arg;
}
var obj1={};
var bar=foo.bind(obj1);
bar(2);
console.log(obj1.a);
var baz=new bar(3);
console.log(obj1.a);//2
console.log(baz.a);//3
2.5 绑定例外
除了上面介绍的4中绑定方法外,还有一些其他的绑定方式。1、被忽略的this
function foo(){
console.log(this.a);
}
var a=3;
foo.call(null);//2
在调用call、apply、bind时,传入null或者undefined时会被忽略,this会指向window。这种调用方式会修改全局对象,可能会导致不可预计的后果。所有这里提供了一种更安全的this方法。
function foo(a,b){
console.log(a+","+b);
}
var a=3;
var φ=Object.create(null);
//φ可以让函数变得更安全,还可以提高代码的可读性。将this的使用限制在这个空对象中,不会对全局产生任何的影响。
foo.apply(φ,[2,3]);//2,3
//柯里化
var bar=foo.bind(φ,2);
bar(3);//2,3
Obect.create(null)是js中创建一个空对象的对简单的方法。该方法返回一个{}对象,但是不会有__proto__属性,比var obj={}更空。
2、间接引用
function foo(){
console.log(this.a);
}
var a=2;
var obj={a:3,foo:foo};
var b={a:4};
obj.foo();//3
(b.foo=obj.foo)();//2
//b.foo=obj.foo返回的是目标函数的引用
上面的代码可以这样理解
var c=b.foo=obj.foo;
c();
注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。
3、软绑定
硬绑定是把this强制绑定到指定的对象(除了new时),防止函数调用默认绑定规则。但是硬绑定会降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显示绑定来修改this。如下面代码所示,不管对foo函数使用隐式绑定还是显示绑定时,调用foo()都不会改变bar()中的this值。
function bar(){
console.log(this.a)
}
var obj={
a:2
}
function foo(){
bar.call(obj)
}
软绑定:给默认绑定指定一个全局对象和undefined以外的值,同时保留隐式绑定或者显示绑定修改this的能力。
Function.prototype.softBind=function(){
let fn=this;
//获取参数
let args=[].slice.call(arguments,1);
let bound = function(){
return fn.apply(!this||this===(window||global)?obj:this,args.concat(...arguments));
}
bound.prototype=Object.create(fn.prototype);
return bound;
}
function foo(){
console.log("a:"+this.a);
}
let obj={
a:1
}
let obj2={
a:2
}
let obj3={
a:3
}
let fooObj=foo.softBind(obj);
fooObj();//a:1
obj2.foo=foo.softBind(obj);
obj2.foo();//a:2;//隐式绑定
fooObj.call(obj3);//a:3 显示绑定
4、箭头函数
箭头函数会继承外层函数调用的this绑定。
function foo(){
setTimeOut(()=>{
console.log(this.a);//2
},1000)
}
var obj={
a:1
}
foo.call(obj);