前言: 在学习javascript的过程中,this的指向一直是一个令人头疼的问题,很多新手包括我自己在内一开始都会对this的指向有这样那样的误解,本文是我在读了《你所不知道的JavaScript 上卷》后,对this的指向有了深一层次的理解,故写下这篇文章,希望能帮助更多人理解this的指向问题。
目录
1.为什么要使用this
在JavaScript的使用过程种,“类”一直是很重要的概念(实际上js中并不存在类),那么如何实现一个“类”呢?如果不使用this的话,那你的代码可能是这样的:
function teacher(name, age) { //创建教师类
var tc = {};
tc.name = name;
tc.age = age;
return tc;
}
function student(name,age) { //创建学生类
var sd = {};
sd.name = name;
sd.age =age;
return sd;
}
//实例化
var teacher1 = teacher('wws', 30);
var student1 = student('frx', 18);
这样写有什么不好吗?嗯。。。好像看起来没啥问题,可是你怎么区分一个对象到底是老师还是学生呢?另外,如果我想teacher类新增一个方法,难道我是给每个实例都添加一个方法吗,还是给又重新对teacher类进行修改?毫无疑问,这些都是挺令人烦燥的事情,那有什么办法可以解决这个问题吗?有!使用this就是一个很好的办法。
下面是改进的代码:
function teacher(name, age) { //创建教师类
this.name = name;
this.age = age;
}
function student(name,age) { //创建学生类
this.name = name;
this.age = age;
}
//实例化
var teacher1 = new teacher('wws', 30);
var student1 = new student('frx', 18);
//给teacher类新增一个方法
teacher.prototype.teach = function() {
console.log('teach');
}
也许你看不懂这段代码,这里使用原型和原型链的相关的知识,你目前只要知道this有这个好处就行了,下面我们来细说this这个似乎是很深奥的问题。
2.this的指向
2.1this的四条指向规则
(1)默认绑定
当函数独立调用时,即没有任何的前缀,this指向window,即全局对象。在严格模式下,this指向undefined。
function foo() {
console.log(this.a);
}
var a = "window";
foo(); //输出: window
(2)隐式绑定
当通过某个对象进行调用时,this指向该对象。当函数调用时有多个前缀时,this指向最靠近函数的那个前缀所代表的对象。
function foo() {
console.log(this.a);
}
var a = "window";
var obj = {
a: "obj",
fn: foo
}
foo(); //输出: window
obj.fn(); //输出: obj
window.obj.fn() //输出:obj
其实本质上foo也可以认为是通过window对象调用,这样理解规则一也许会更加容易。
(3)显示绑定(即apply,call,bind的使用)
JavaScript中存在一些方法可以改变this的指向,比如call(...),apply(...),bind(...)。下面我会细说这三个方法:
call方法和apply方法在改变this的指向上作用时一致的,只不过它们的使用方法有所不同而已,但是这两个函数的第一个参数都是一个对象,这个就是给this准备的,在调用函数时将其绑定到this上。 下面是call方法和apply方法的使用方法:
call(obj:需要绑定到的对象,arg1:传入到函数的第一个参数,arg2:传入到函数的第二个参数,...)
apply(obj:需要绑定到的对象,array: 传入函数的参数数组)
举个栗子(注意看函数使用方法):
function foo(name) {
console.log(name);
console.log(this.age);
}
var age = 30;
var person = {
age: 18
}
foo('wws'); //输出: wws 30
foo.call(person, 'frx'); //输出: frx 18
foo.apply(person, ['frx']); //输出: frx 18
你肯定觉得奇怪,bind呢?别急,bind就是我接下来要说的硬绑定。硬绑定也是显式绑定的一种,但是当使用硬式绑定后,this的指向就不能通过call,apply,bind进行重复绑定了。那么bind的使用方法是什么?其实它和call的使用方式是一样的,但它又有所不同,使用call和apply的函数会立即执行,但bind返回的是一个新的函数,返回的函数不能够再使用apply,call,bind来显式绑定this的指向了。
bind(obj:需要绑定到的对象,arg1:传入到函数的第一个参数,arg2:传入到函数的第二个参数,...)
说到这,你是不是又有点懵了?没关系,继续看下一个栗子:
function foo() {
console.log(this.name);
}
var name = 'window';
var person1 = {
name: 'person1'
};
var person2 = {
name : 'person2'
}
var bar = foo.bind(person1); //使用bind进行绑定
bar(); //person1
//尝试使用bind重新绑定bar中this的指向(严格模式会报错)
bar.bind(person2)(); //person1
可以看到我们尝试使用bind对已经硬绑定的函数进行绑定,但返回值仍然是person1,所以再次强调,使用硬绑定的函数的this的指向不能够再使用apply,call,bind来进行重新绑定。
(4)new绑定
除了上述的3中绑定,我们使用new操作符时也会更改this的指向,因为new操作符的过程如下:
① 新创建一个对象。
② 将该对象的原型修改为函数的portotype属性(注意prototype和[[prototype]]不是同一个东西)。
③ 将函数调用的this绑定到这个新创建的对象上。
④ 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新创建的对象。
废话少说,上代码:
function Foo(name) {
this.name = name;
}
var person = new Foo('frx'); //使用new操作符更改this的指向
console.log(person.name);
2.2 规则的优先级
介绍完上面的四条规则,也许你会有疑惑,如果一个函数同时使用了bind,有使用了new操作符,那么this的指向该是什么样子的呢?接下来我们就讲述规则的优先级。
毫无疑问,默认绑定的优先级是四条规则中最低的,我们先不考虑它。
隐式绑定和显式绑定谁的优先级高的呢,也许你已经猜到了,但实践是检验真理的唯一标准嘛!我们还是来比较一下吧!
function foo() {
console.log(this.name);
}
const person1 = {
name: 'frx',
foo: foo
}
const person2 = {
name: 'gp'
}
person1.foo(); //frx
person1.foo.call(person2); //gp
可以看到我们是call显示绑定后,this的指向由person1变成了person2,所以显示绑定的优先级比隐式绑定的优先级高。
那么new绑定和隐式绑定谁的优先级高呢,我们再来比较一下:
function foo(name) {
this.name = name;
}
const obj = {
foo: foo
}
obj.foo('frx');
console.log(obj.name); //frx
var bar = new obj.foo('gp');
console.log(obj.name); //frx
console.log(bar.name); //gp
可以看到,我们先调用了obj.foo(),因为this指向obj,所以该方法给obj添加了一个新的属性name,接着我们使用new绑定,发现foo中的this已经被修改,所以bar.name就是我们传进去的参数,如果this仍然指向obj的话,obj.name应该被修改为gp。故new绑定的优先级比隐式绑定的优先级更高。
好了,最后一个问题new绑定和显示绑定谁的优先级更高?由于new不能和apply/call一起使用,所以我们使用硬绑定bind来和new绑定进行比较。
function foo(name) {
this.name = name;
}
const person1 = {};
//使用bind绑定
const bar = foo.bind(person1);
bar('frx');
console.log(person1.name); //frx
//使用new绑定
var person2 = new foo('gp');
console.log(person1.name); //frx
console.log(person2.name); //gp
可以看到,我们先使用了bind对foo进行硬绑定到person1上,接着使用new绑定,如果new绑定的优先级比显示绑定的优先级低的话,person1.name应该被更改为gp,所以new绑定的优先级比显示绑定的优先级高。
2.3 判断this(ES5)
判断this的指向的顺序:
(1)函数是否是在new中调用?如果是,this指向新创建的对象。
(2)函数是否使用了call、apply或硬式绑定?如果是,this指向指定的对象。
(3)函数是否通过某个对象调用(隐式绑定,即在某个上下文中调用),如果是,this指向那个上下文对象。
(4)如果都不是,在严格模式下,this指向undefined,非严格模式下指向window。
不过凡事总有例外,this的指向也一样。
2.4绑定例外
当我们使用显示绑定时,如果我们绑定的对象是null或者undefined的话,那么这个参数会被忽略,并且会将this绑定到window上。如:
function foo() {
console.log(this.a);
}
var a = 'window';
foo.call(null); //window
3.箭头函数的this
箭头函数的this会根据当前词法作用域来决定,也就是说箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。比如:
function foo() {
return () => {
console.log(this.a);
}
}
var a = 'window'
var obj = {
a: 'obj'
}
foo.call(obj)(); //obj
foo()(); //window
上述箭头函数的this就根据foo中的this的指向确定的,foo中this指向谁,那么箭头函数中的this就指向谁,所以它和下面这段代码是等价的。
function foo() {
var that = this;
return function() {
console.log(that.a);
}
}
var a = 'window'
var obj = {
a: 'obj'
}
foo.call(obj)(); //obj
foo()(); //window
好了,相信你读到这对this的指向已经有了更深层次的理解了吧,如果有用的话帮忙点个赞哦o(* ̄▽ ̄*)ブ!