this 因何存在
看下面的代码:
function person() {
return this.name;
}
function speak() {
return `hello ${person.call(this)}`;
}
let me = {
name: "lilei"
}
let you = {
name: "hanmeimei"
}
person.call(me);
person.call(you);
speak.call(me);
speak.call(you);
输出如下:
通过上面可以发现无需传递形参就可以返回不同的结果。为什么?因为this提供了隐式传递参数的机制,传递的内容是某个对象的地址。如果不使用this就需要显示传递形参,当开发大型项目时参数过多会导致混乱不堪,而把参数挂到this对象上就避免了这种情况,代码更加简洁,但可读性会降低,初接触可能产生困惑。从更抽象角度看,this成为了连接对象和函数的桥梁。
this 如何绑定
默认绑定
默认绑定的意思是当不满足其他绑定规则时,执行默认绑定规则。默认绑定会把this绑定到全局对象(浏览器里面是window对象),代码如下:
let num = 0;
function foo() {
let num = 1;
return this.num;
}
foo(); // undefined
输出如下:
上面的代码实现了默认绑定,从Scope->Local里我们看到,最终this指向window对象,由于使用let定义num变量,故在window上访问num值为undefined。而值为0的num存在于Scope->Script中。
隐式绑定(上下文绑定)
函数的地址被某个对象的属性引用,并通过对象的属性运行该函数(对象提供了上下文),就会触发this的隐式绑定,this会被绑定成当前对象。
let obj = {
name: "shadow",
foo: foo
}
function foo() {
debugger;
return this.name;
}
obj.foo(); // shadow
输出如下:
在此补充一点,不管对象嵌套多深,this只会绑定为直接引用该函数地址的对象。
隐式绑定的丢失
代码如下:
let obj = {
name: "shadow",
foo: foo
}
function foo() {
return this.name;
}
function doFoo(fn) {
fn();
}
doFoo(obj.foo); // undefined
输出如下:
当执行doFoo函数时,函数形参fn接收实参obj.foo,这里有一个赋值操作,相当于执行了fn=obj.foo,obj.foo指向foo函数的地址,fn函数由此获得了foo函数的地址,这个很重要,因为fn函数执行离开了obj对象的属性直接运行。输出如下:
在函数调用栈执行到foo函数时,由于丢失了和上下文obj对象的关系,因此foo函数的Scope->Local->this指向window,即执行了默认绑定。通过赋值导致隐式绑定关系丢失很常见,需要格外注意。
显示绑定
Function对象的prototype上提供了3个方法来显示绑定this,分别是call、apply、bind方法,三个方法的简单实现如下:
Function.prototype.call2 = function(ctx, ...args) { // ...为rest运算符,接收若干形参
ctx = ctx || window;
ctx.fn = this; // ctx.fn指向当前函数
let rs = ctx.fn(...args); // 以隐式绑定方式调用当前函数
delete ctx.fn; // 避免对实参ctx产生影响
return rs;
}
Function.prototype.apply2 = function(ctx, args) { // args为数组参数
ctx = ctx || window;
ctx.fn = this; // ctx.fn指向当前函数
let rs = ctx.fn(...args); // 以隐式绑定方式调用当前函数
delete ctx.fn; // 避免对实参ctx产生影响
return rs;
}
Function.prototype.bind2 = function(ctx, ...args) { // ...为rest运算符,接收若干形参
ctx = ctx || window;
return () => {
return this.apply(ctx, args);
}
}
new 绑定
js中的构造函数和传统oop语言(例如:java)中的构造函数执行原理不一样。java中执行new ClassName时调用ClassName中的构造函数(构造函数又可能调用父类的构造函数,通过super调用)创建一个新的对象。js中的构造函数被new操作符调用时,它不属于某个ClassName,而只是一个函数。
使用new操作符调用构造函数会执行四步:
- 创建一个新对象;
- 新对象的隐式原型(proto)指向构造函数显式原型(prototype)的地址;
- 构造函数的this指向新对象;
- 构造函数如果有返回值并且返回值为对象,则返回对象(可实现单例模式),否则返回新对象;
代码如下:
function Foo(name) {
this.name = name;
}
Foo.prototype.getName = function() {
return this.name;
}
let foo = new Foo("shadow");
foo.getName(); // shadow
foo.__proto__; // {getName: ƒ, constructor: ƒ, ...}
Foo.prototype; // {getName: ƒ, constructor: ƒ, ...}
foo.__proto__ === Foo.prototype; // true
输出如下:
使用构造函数实现单例模式,代码如下:
let instance;
function Singleton(name) {
if(instance) return instance;
instance = this;
instance.name = name;
return instance;
}
Singleton.prototype.getName = function() {
return this.name;
}
let singleton = new Singleton("shadow");
let singleton2 = new Singleton("shadow2");
singleton.getName(); // shadow
singleton2.getName(); // shadow
singleton === singleton2; // true
this优先级
new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定。
es6 中的 this
es6中的this可以通过箭头函数直接绑定在该函数执行作用域上(不是上下文)。代码如下:
function foo() {
return () => {
return this.name;
}
}
let obj = {
name: "shadow"
}
let faith = {
name: "faith"
}
let bar = foo.call(obj);
bar.call(faith); // shadow
输出如下:
foo函数的执行作用域为obj对象(call函数显示绑定),因此foo内箭头函数的Scope->Local->this指向obj对象。