关于 this
this 误解
- 指向自身
- 它的作用域
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 的调用位置
注意我们是如何(从调用栈中)分析出真正的调用位置的,因为它决定 this 的绑定。
绑定规则
看绑定规则之前,必须先找到调用位置。然后再判断需要应用下面四条规则中的哪一条。
默认绑定
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
可以通过分析调用位置来看看 foo() 是如何调用的。 foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用 默认绑定,无法应用其他规则。
注意,严格模式下,无法使用默认绑定。
隐式绑定
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
调用位置在 obj 上下文中,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。
隐式丢失
一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上。
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.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"
JavaScript 环境中内置的 setTimeout() 函数实现和下面的伪代码类似:
function setTimeout(fn,delay) {
// 等待 delay 毫秒
fn(); // <-- 调用位置!
}
显式绑定
call 和 apply
它们第一个参数是一个对象,它们会把这个对象绑定到 this,接着在调用函数时执行这个 this。
可惜,显式绑定仍然无法解决我们之前提出的丢失绑定问题。
硬绑定
但是显式绑定的一个变种可以解决这个问题。
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
硬绑定的应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值。
bind 就是硬绑定。
bind() 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。
new 绑定
javascript 中的 new 的机制实际上和面向类的语言完全不同。
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
- 创建一个全新的对象
- 这个新对象会被执行 [[ 原型 ]] 连接
- 这个新对象会绑定到函数调用的 this
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
根据描述,手写一个new
function NEWAPI(...args) {
// 提取第一个参数,作为函数调用
const fn = args.shift();
// 构建新的对象,并且将fn的prototype绑定到当前对象的__proto__
const context = Object.create({}, fn.prototype);
// 新对象绑定到函数调用的 this
const res = fn.apply(context, args);
// 如果函数没有返回对象,就返回context
return typeof res === 'object' ? res : context;
}
function Person(name) {
this.name = name;
}
new Person('tony')
NEWAPI(Person, 'tom')
优先级
new > 隐式
显式 > 隐式
判断 this
- 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象
var bar = new foo()
- 函数是否通过 call, apply 或者硬绑定调用?如果是的话,this 绑定的是指定的对象。
var bar = obj1.foo()
- 函数是否在某个上下文对象中调用?如果是的话,this 绑定的是那个上下文对象
var bar = obj1.foo()
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到全局对象。
var bar = foo();
箭头函数
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)作用域来决定 this
function foo() {
var self = this; // lexical capture of this
setTimeout( function(){
console.log( self.a );
}, 100 );
}
var obj = {
a: 2
};
foo.call( obj ); // 2
用外层的 this
小结
- 先确定调用位置
- 规则
- 默认绑定
- 显式绑定(call, apply)
- 硬绑定(bind)
- new
- new处理过程
- 生成新对象
- 将函数的prototype绑定到新对象的原型
- 将新对象绑定为函数的this调用
- 如果函数返回值为非对象,则返回新对象
- 隐藏的坑
// 1
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"
// 2
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"
- 箭头函数
将外层的this,拿到里面来用
function foo() {
let self = this;
setTimeout(function() {
console.log(self.name)
}, 100)
}
foo.call({name:'tom'})
-
面试题
var x = 10;
var obj = {
x: 20,
f: function(){
console.log(this.x); // ?
var foo = function(){
console.log(this.x);
}
foo(); // ?
}
};
obj.f();
答案 20 10
function foo(arg){
this.a = arg;
return this
};
var a = foo(1);
var b = foo(10);
console.log(a.a); // ?
console.log(b.a); // ?
答案 : undefined 10
var x = 10;
var obj = {
x: 20,
f: function(){ console.log(this.x); }
};
var bar = obj.f;
var obj2 = {
x: 30,
f: obj.f
}
obj.f();
bar();
obj2.f();
答案:20 10 30
function foo() {
getName = function () { console.log (1); };
return this;
}
foo.getName = function () { console.log(2);};
foo.prototype.getName = function () { console.log(3);};
var getName = function () { console.log(4);};
function getName () { console.log(5);}
foo.getName(); // ?
getName(); // ?
foo().getName(); // ?
getName(); // ?
new foo.getName(); // ?
new foo().getName(); // ?
new new foo().getName(); // ?
答案:2 4 1 1 2 3 3
问题出处链接