在js中很多地方都用到this,结合最近的读书,做个总结。

关于this

  • 随着函数使用场合不同,this的值会发生变化。
  • this对象是在运行时基于函数的执行环境绑定的。在网页的全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。
  • 也就是说this关键字总是指代调用者。

调用位置
函数在代码中被调用的位置(而不是声明的位置)

可以通过分析调用栈(为了达到当前执行位置所调用的所有函数)来获取调用位置,而this只与当前正在执行的函数的前一个调用有关。

绑定方式

默认绑定

独立函数调用(不带任何修饰的函数引用)时根据调用位置来判断

1
2
3
4
5
function foo(){
console.log(this.a);
}
var a = 2;
foo();//2

注:如果函数运行处于严格模式(strict mode)(与函数调用位置无关),全局对象将无法使用默认绑定,this会绑定到undefined

1
2
3
4
5
6
7
8
function foo(){
console.log(this.a);
}
var a = 2;
(function(){
"use strict";
foo();//2
})();

隐式绑定

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。
对象属性引用链中只有最顶层或者说最后一层会影响调用位置

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(){
console.log(this.a);
}

var obj2 = {
a:42,
foo:foo
};

var obj1 = {
a:2,
obj2:obj2
};


obj1.obj2.foo();//42
隐式丢失

一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,这取决于是否是严格模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};


//1. 函数别名时
var bar = obj.foo;//函数别名
var a = "oops,global";//a 是全局对象的属性
bar();//"oops,global"
//bar实际上引用的是foo函数本身,并在全局调用它


//2. 作为行参时
function doFoo(fn){//隐式赋值,和函数别名一样
fn();
}
doFoo(obj.foo);

//调用内置回调函数时,和作为行参时没有区别
setTimeout(obj.foo,100);//"oops,global"

显式绑定

call()apply()方法
第一个参数是对象,它们会把这个对象绑定到this,接着在调用函数时指定这个this

硬绑定

显式的强制绑定
创建一个包裹函数,传入所有的参数并返回接受到的所有值
es5提供了Function.prototype.bind()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if(!Function.prototype.bind){
Function.prototype.bind = Function(oThis){
if(typeof this != 'function'){
//与es5最接近的内部IsCallable函数
throw new TypeError("Function.prototype.bind - what is traying to be bound is not callable");
}
var aArgs = Array.prototype.slice.call(arguments,1),
fToBind = this,
fNOP = function(){},
fBound = function(){
return fToBind.apply(
this instanceof FNOP &&
oThis ? this : oThis
),
aArgs.concat(Array.prototype.slice.call(arguments));
};
//判断硬绑定函数是否是被new调用,如果是的话就会使用新创建的this替换硬绑定的this
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
API调用的“上下文”

第三方库的许多函数,以及js语言和宿主环境中许多新的内置函数,都提供了一个可选的上下文参数。

new绑定

使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:

  1. 创建(或者说构造)一个全新的对象
  2. 这个新对象会被执行 [[原型]] 连接
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

优先级

判断this流程:

  1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
    var bar = new foo()
  2. 函数是否通过callapply(显式绑定)或者硬绑定调用?如果是的话this绑定的是指定的对象。
    var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文。
    var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
    var bar = foo()

改进

硬绑定把this强制绑定到指定的对象(除了使用new时),防止函数调用应用默认绑定规则,但却降低了函数的灵活性,使用硬绑定后就无法使用隐式或者显式绑定去修改this

如果可以给默认绑定指定一个全局对象和undefined以外的值,那就可以实现和硬绑定相同的效果,同时保留隐式或显式绑定修改this的能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(!Function.prototype.softBind){
Function.prototype.softBind = function(obj){
var fn = this;
//捕获所有curried参数
var curried = [].slice.call(arguments,1);
var bound = function(){
return fn.apply(
!this || this === (window || global)?obj:this;
curried.concat.apply(curried,arguments)
);
}
bound.prototype = Object.create(fn.prototype);
return bound;
}
}

例外

被忽略的this

如果把null或者undefined作为this的绑定对象传入call,apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
安全处理:DMZ对象Object.create(null)
Object.create(null){}很像,但是并不会创建Object.prototype这个委托。

间接引用

容易发生在赋值时

1
2
3
4
5
6
7
8
9
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。具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。这其实和self = this机制一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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, 不是3!

foo()内部创建的箭头函数会捕获调用时foo()this。由于foo()this绑定到obj1bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new也不行!)

和普通函数的区别
  • 不能创建实例
1
2
3
4
5
var F = () => {
};
console.log(typeof F); // function
console.log(F.constructor) // [Function: Function]
console.log(new F) // TypeError: () => {} is not a constructor
  • this关键字与父级共享
  • this指向不会改变,即不使用this的四种标准规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var obj = {
count: 10
};
function Counter() {
this.count = 0;
this.add = (d) => {
return setTimeout(() => {
// this关键字与父级共享
console.log('this', this);
console.log('increase', ++this.count);
}, d);
};
this.get = () => this.count;
}
var counter = new Counter();
counter.add(1000);
// 不改变this指向
console.log('change this', counter.get.call(obj));
//change this 0
//this Counter {count: 0}
//increase 1
  • 不能使用在generator
1
2
var fun = *() =>{//...}
//Uncaught SyntaxError
  • 使用arguments在浏览器中报错,用…args代替,argsarray,在普通函数中arguments是类数组。
1
2
3
4
5
var f = (...args) => {
console.log(args.constructor); // function Array(){}
return args;
};
console.log(f(1,2,[3]));//[1, 2, Array[1]]
应用

箭头函数最常用于回调函数中,例如事件处理器或者定时器:

1
2
3
4
5
6
7
8
9
10
function foo() {
setTimeout(() => {
// 这里的this在此法上继承自foo()
console.log( this.a );
},100);
}
var obj = {
a:2
};
foo.call( obj ); // 2

参考

你不知道的javascript
ES6的那些事儿