JS基础学习——this
函数的this绑定是JS中最容易出错的点,想要清楚知道this指向,必先记住this的固定绑定规则。
JS中this有四种不同的绑定规则:默认绑定、隐式绑定、显式绑定、new绑定,函数应用的绑定规则主要由它的调用方式决定。下面依次介绍了JS的四种函数调用方法、this的五种绑定规则、this的五种绑定规则的优先级顺序、其他一些特殊情况。
JS的函数调用方式
函数只有被调用时,才会被执行。JS共有四种函数调用方式:函数调用、方法调用、间接调用、构造函数调用。
- 函数调用
最简单的函数调用方式,不加任何修饰,直接利用函数引用调用函数,如code 1所示。嵌套函数调用容易误解成方法调用,但其实它就是函数调用,比如code 2-code 4;
/*-----------code 1----------*/
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
/*-----------code 2----------*/
//虽然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
/*-----------code 3 IIFE----------*/
var a = 0;
function foo(){
(function test(){
console.log(this.a);
})()
};
var obj = {
a : 2,
foo:foo
}
obj.foo();//0
/*-----------code 4 闭包----------*/
var a = 0;
function foo(){
function test(){
console.log(this.a);
}
return test;
};
var obj = {
a : 2,
foo:foo
}
obj.foo()();//0
- 方法调用
当一个函数被附加为一个对象的属性时,我们称这个函数是对象的方法,方法调用就是指以"对象.方法()"的格式调用函数,如code 5所示。其实对象实际拥有的只是方法的引用,而非方法本身。
/*-----------code 5----------*/
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
- 间接调用
JS中函数也拥有方法,函数可以call和apply方法来调用自己,同时通过函数的第一个参数为自己这次执行指定this对象。如code 6所示,call和apply的功能是一样的,只是传入参数的格式有所区别:call([thisObj[,arg1[, arg2[, [,.argN]]]]])、apply([thisObj[,argArray]])。
/*-----------code 6----------*/
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
//or
foo.apply( obj ); // 2
还有一个和间接调用相关的方法是bind,它也可以为函数绑定this,但call和apply会立即执行函数,而bind只是返回一个有this绑定的新函数,它也是利用第一个参数设置this绑定,第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。如code 7所示。
/*-----------code 7----------*/
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
};
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
- 构造函数调用
在函数调用或方法调用之前加上new关键词就构成了构造函数调用。构造函数用来初始化对象,一般不存在return,此时构造函数的表达式结果是新对象的值,如code 8所示;如果构造函数没有返回值,或是返回值是一个原始值,此时调用结果也是新对象的值,如code 9所示;如果构造函数的返回值是一个对象,那么调用结果就是这个返回值对象,如code 10所示。
/*-----------code 8----------*/
function fn(){
this.a = 2;
}
var test = new fn();
console.log(test);//{a:2}
/*-----------code 9----------*/
function fn(){
this.a = 2;
return;
}
var test = new fn();
console.log(test);//{a:2}
/*-----------code 10----------*/
var obj = {a:1};
function fn(){
this.a = 2;
return obj;
}
var test = new fn();
console.log(test);//{a:1}
this的四种绑定规则
一、默认绑定
用函数调用方式执行函数时,遵循默认绑定,在非严格模式下,this绑定window,严格模式下,this为undefined,如code 11所示。
/*-----------code 11----------*/
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: `this` is `undefined`
二、隐式绑定
用方法调用方式执行函数时,遵循隐式绑定,this绑定函数的对象,如code 5所示,注意在级联调用的情况下,this绑定的是直接调用函数的对象,如code 12所示。
/*-----------code 12----------*/
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
隐式绑定中函数丢失绑定对象的现象称为隐式丢失,当隐式丢失时,此时this将遵循默认绑定规则,常见隐式丢失的情况有三种:函数别名、参数传递、立即调用,如code 13 - code 14所示。
其中前两种情况都是由于引用传递导致的隐式丢失,在隐式调用中,对象拥有的只是函数的引用,这个引用只能通过对象才能被调用的,一旦这个引用通过赋值方式传递给另一个变量,就创建了函数一个新的引用,这个新的引用不再从属于对象,调用它算是函数调用,因此遵循默认绑定规则。
/*-----------code 13 函数别名----------*/
var a = 0;
function foo(){
console.log(this.a);
};
var obj = {
a : 2,
foo:foo
}
var bar = obj.foo;
bar();//0
/*-----------code 14 参数传递----------*/
var a = 0;
function foo(){
console.log(this.a);
};
function bar(fn){
fn();
}
var obj = {
a : 2,
foo:foo
}
//把obj.foo当作参数传递给bar函数时,有隐式的函数赋值fn=obj.foo。
bar(obj.foo);//0
立即调用的情况比较特殊,如果赋值和执行语句合并则相当于函数调用foo(),将遵循默认绑定规则,但如果赋值之后再调用p.foo(),就算是p对象的方法调用了,将执行隐式绑定规则。
/*-----------code 15 立即调用----------*/
function foo() {
console.log( this.a );
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
//赋值和执行语句合并,将o.foo函数赋值给p.foo函数,然后立即执行。相当于仅仅是foo()函数的立即执行
(p.foo = o.foo)(); // 2
//赋值和执行语句分开
p.foo = o.foo;
p.foo();//4
三、显示绑定
用间接调用方式执行函数时,遵循显示绑定,this由call、apply、bind方法的第一个参数决定。
当函数并不关心this绑定,第一个参数的值可能会是null,此时函数遵循默认绑定规则,如code 16所示。但是这样处理并不安全,更加适合的方式是让函数的this绑定到一个空的对象,如code 17所示。
/*-----------code 16----------*/
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// spreading out array as parameters
foo.apply( null, [2, 3] ); // a:2, b:3
// currying with `bind(..)`
var bar = foo.bind( null, 2 );
bar( 3 ); // a:2, b:3
/*-----------code 17----------*/
function foo(a,b) {
console.log( "a:" + a + ", b:" + b );
}
// our DMZ empty object
var ø = Object.create( null );
// spreading out array as parameters
foo.apply( ø, [2, 3] ); // a:2, b:3
// currying with `bind(..)`
var bar = foo.bind( ø, 2
四、new绑定
用构造函数调用方式执行函数时,遵循new绑定,this为new创建的新对象,大多数情况下,new返回的就是这个新对象的引用,比如code 8,9所示;但是当函数的return结果是一个对象类型的话,new返回的就是这个return结果的引用,比如code 10所示。
this的四种绑定规则的优先级
四种绑定规则的优先级为: new绑定 > 显示绑定 > 隐式绑定 > 默认绑定,验证例子如下。
/*-----------code 18 显示绑定 > 隐式绑定----------*/
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
//在该语句中,显式绑定call(obj2)和隐式绑定obj1.foo同时出现,最终结果为3,说明被绑定到了obj2中
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
/*-----------code 19 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
//在下列代码中,隐式绑定obj1.foo和new绑定同时出现。最终obj1.a结果是2,而bar.a结果是4,说明this被绑定在bar上
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
/*-----------code 20 new绑定 > 显式绑定----------*/
function foo(something) {
this.a = something;
}
var obj1 = {};
//先将obj1绑定到foo函数中,此时this值为obj1
var bar = foo.bind( obj1 );
bar( 2 );
console.log(obj1.a); // 2
//通过new绑定,此时this值为baz
var baz = new bar( 3 );
console.log( obj1.a ); // 2
//说明使用new绑定时,在bar函数内,无论this指向obj1有没有生效,最终this都指向新创建的对象baz
console.log( baz.a ); // 3
其他情况
一、硬绑定
前面几种情况的this绑定都是可以再修改的,但在硬绑定的情况下,this绑定后不能再被修改,它的格式如code 21、code 22所示,就是将函数调用语句嵌套到另一个函数里面,这样函数bar的外部语句对bar进行的所有this绑定修改,都只是修改bar的this绑定,由于this绑定不会因为函数嵌套而发生传递的,因此foo的this绑定永远由其自身的调用方式决定,不再发生改变。
/*-----------code 21----------*/
var a = 0;
function foo(){
console.log(this.a);
}
var obj = {
a:2
};
var bar= function(){
foo.call(obj);
}
//无论之后如何调用函数bar,foo的this绑定都不会发生修改
bar();//2
setTimeout(bar,100);//2
bar.call(window);//2
/*-----------code 22----------*/
var a = 0;
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
var bar= function(){
obj.foo();
}
//无论之后如何调用函数bar,foo的this绑定都不会发生修改
bar();//2
setTimeout(bar,100);//2
bar.call(window);//2
二、箭头函数
内部嵌套函数的调用往往是函数调用,遵循默认绑定规则,this指向window或是undefined,但有的时候内部嵌套函数希望继承的是外部函数的this绑定,那应该怎么去实现呢?
一种方法是通过作用域嵌套查找的方式传递this绑定,将外部this值赋值给一个新的变量,内部函数可以通过这个变量访问到外部函数的this值,如code 23所示,
/*-----------code 23----------*/
var a = 0;
function foo(){
var that = this;
function test(){
console.log(that.a);
}
return test;
};
var obj = {
a : 2,
foo:foo
}
obj.foo()();//2
第二种方法就是箭头函数,它会在外部函数被调用的时候,自动继承外部函数的this绑定,且一旦绑定,4种绑定规则都无法对其绑定进行修改。如code 24所示。
/*-----------code 24----------*/
var a = 0;
function foo(){
var test = () => {
console.log(this.a);
}
return test;
};
var obj = {
a : 2,
foo:foo
}
obj.foo()();//2
需要注意,箭头函数没有名称,箭头前面的括号里面可以包含参数;箭头函数不可以当作构造函数,也就是不可以在箭头函数前面加上new关键词,否则会报错;箭头函数中不存在arguments对象。
虽然箭头函数可以把作用域和this机制联系起来,但是却容易混淆,使代码难以维护。必要时还是使用bind方法,尽量避免使用that=this和箭头函数。
[2] 深入理解this机制系列第一篇——this的4种绑定规则
[3] 深入理解javascript函数系列第一篇——函数概述