系列文章:
this 机制
误解
this 机制在 javascript 中是动态绑定,或称为运行期绑定的。这就导致 JS 中的 this 关键字会有多重含义,所以会给我们造成一误解。学习 this 的第一步是明白this既不指向函数自身也不指向函数的词法作用域。
指向自身
人们容易把 this 理解成指向函数自身,看下面的代码
function foo(num) {
console.log("foo: " + num
this.count++;
}
// 这里为 foo 添加了一个属性 count,初始化为 0
foo.count = 0;
for(var i = 0; i < 10; i++) {
if (i > 5) {
foo(i);
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
console.log(foo.count); // 0
// foo 确实被调用了4次,但是 foo.count 还是 0,
// 是因为函数内部的 this 并不是指向那个函数对象,
// 所以虽然属性名相同,但是根对象却不相同
指向它的作用域
this指向函数的作用域,这个问题比较复杂,因为有时它是正确的,有时它的错误的。但可以明确的是:this在任何情况下,都不指向函数的词法作用域。
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo();
结果是 undefined ,因为这里通过 this.bar() 来引用 bar 函数,这里能调用成功是因为:
- foo()被调用时,this === window,所以可以调用 bar 函数。
- bar执行时,this === window,使用对象属性访问规则,没有找到 a,所以返回undefined。
⚠️注意:这里有人会认为结果是 Reference Error 报错,其实和 RHS(Javascript进阶1--作用域和闭包 内有解释)混淆了。
this 解析
javascript中,this 是在运行时进行绑定的,它的上下文取决于函数调用时的各种条件。
绑定规则
先根据把绑定的优先级抛出结论,按照以下顺序进行判断:
-
函数是否在 new 中调用(new 绑定)?如果是的话,this 绑定的是新创建的对象。
var bar = new Foo() -
函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话, this 绑定的是指定的对象。
var bar = foo.call(obj2) -
函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。
var bar = obj1.foo() -
如果都不是,使用默认绑定,严格模式下,绑定到 undefined,否则绑定到全局对象。
var bar = foo()
new 绑定
在传统的面向类的语言中,构造函数是类的一些特殊方法,使用 new 初始化类时,会调用类中的构造函数。
首先,我们需要澄清的一个误解是:在JS中,是没有构造函数的,在普通的函数调用前面加 new 关键字之后,就会把这个函数调用变成一个“构造函数调用”。实际上,new 会劫持所有普通函数并用构造函数的形式来调用它。
那么在JS中,使用 new 来调用函数,var obj = new Foo(),会执行以下操作:
-
首先创建一个全新的空对象。
var obj = {} -
将空对象的原型 [[prototype]] 赋值为构造函数的原型.
obj.__proto__ = Foo.prototype -
更改 this 的指向。
Foo.call(obj) -
若 return 有值,并且值是对象,则直接返回此对象,否则,返回新创建的对象 obj。
现在看一段简单的代码检测一下学习成果
function foo1(a) {
this.a = a;
}
function foo2(name) {
this.name = name;
return {
w:1
};
}
foo2.prototype.getName = function() {
return this.name;
};
var bar = new foo1(2);
console.log(bar.a) // 2
var a = new foo2('hh');
a.getName();
// Uncaught TypeError: a.getName is not a function
// 这是因为,foo2 有返回值,并且是一个对象,所以a值是 {w: 1}
显式绑定和硬绑定
如果想调用函数时,强制把函数的 this 绑定到 obj 上,可以使用 call(...) 和 apply(...) 方法,它们的第一个参数一个对象,是给 this 准备的,这称之为显式绑定。
function foo() {
console.log(this.a)
}
var obj = {
a: 2
};
foo.call(obj) //2
⚠️注意:如果传入了一个原始值(布尔、数字、字符串等类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new Boolean(...)、new Number(...)、new String(...))。
硬绑定就是显式绑定的一个变种,用于我们将 this 被永久绑定到 obj 的 foo 函数,这样我们就不必每次调用 foo 都在尾巴上加上 call 那么麻烦。
function foo() {
console.log(this.a)
}
var obj = {
a: 2
};
var bindFunc = foo.bind(obj)
bindFunc(); //2
call/apply 与 bind 的区别是:call、apply将立即执行该函数,bind 不执行函数,只返回一个可供执行的函数。
隐式绑定
当一个函数被一个对象调用时,会把函数调用中的 this 绑定到这个上下文对象中。
fucntion foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo() //2
⚠️注意:当我们使用隐式绑定规则时,要注意下面两点:
- 在参数传递、赋值时,隐式绑定的函数可能会丢失绑定对象,也就是会应用默认绑定规则。
function foo() { console.log(this.a); } function doFoo(fn) { fn(); } var obj = { a: 2, foo: foo }; var a = 'oops, global'; // obj.foo 引用 foo 函数本身,应用默认绑定 doFoo(obj.foo); // oops, global var bar = obj.foo; // bar 引用 foo 函数本身,应用默认绑定 bar(); // oops, global - 对象属性引用链中只有最后一层在调用位置中起作用。
默认绑定
当直接使用不带任何修饰的函数引用进行函数调用时,只能使用默认绑定,无法应用其他规则。在严格模式下,this 会绑定到 undefined,在非严格模式下, this会绑定到全局对象。
如果把 null 或者 undefined 作为 this 的绑定对象传入 call、apply或者 bind,会应用默认绑定规则。
箭头函数
箭头函数根据外层(函数或全局)作用域来决定 this。
function foo() {
return (a) => {
// 继承自 foo()
console.log(this.a)
};
}
var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.call(obj1);
bar.call(obj2) // 2
⚠️注意:箭头函数的绑定无法被修改
好题练习
练习题1:
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(); //2
// 变量声明提升,赋值语句留在原地,所以结果是4
getName(); //4
// 在Foo函数中,重写了全局作用域下的 getName 函数
Foo().getName(); //1
getName(); //1
// 等价于new (Foo.getName)(), 运算符优先级:成员访问 > new(不带括号) > 函数调用
new Foo.getName(); //2
// 等价于 (new Foo()).getName(),运算符优先级:成员访问、new(带括号)的优先级一样,所以从左到右执行。
// 使用 new 实例 Foo 后生成的实例上,只有原型方法,没有静态方法
new Foo().getName();
练习题2:
var title = 'world';
var a = {
title: 'hello',
alias: this.title,
show() {
console.log(this.title, this.alias)
},
ashow: () => {
console.log(this.title, this.alias)
}
}
a.show(); // hello world
var b = a.show;
b(); // world undefined
a.ashow(); // world undefined
练习题3:
var b = 20;
var a = {
b: 15,
fn: function() {
var b = 30;
return function() {
return this.b;
};
}
};
console.log(a.fn()()); // 20