js中this的总结

1,619 阅读8分钟

前言:

js中this问题是学习前端基础的重要部分,如果想要扎实自己的基础,这块硬骨头就必须啃掉~
在js实际开发过程中我们一定要搞清楚this的指向以及更改this的指向,高效的完成开发任务。

this指向分类

为了能够一眼看出this指向的是什么,我们需要确定它的绑定规则是哪个?this有以下五种规则分类:

  • 默认绑定
  • 隐式绑定
  • 显示绑定
  • new绑定
  • 箭头函数this绑定

默认绑定

第一种是无绑定状态也就是默认绑定状态,经常是独立的函数中会使用到

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

上面函数就应用了this的默认绑定,foo()前面没有调用它的对象,在非严格模式foo()函数会挂载在window上所以函数中的this指向的是全局对象window。在严格模式下禁止了this关键字指向全局对象,this指向undefined,undefined上没有this对象,会抛出错误。

隐式绑定

考虑调用位置是否有调用的对象,如果有会指向调用对象

function foo(){
    console.log(this.a);
}
var obj = {
    a:2,
    foo:foo
}
obj.foo();//2

因为调用foo的时候前面加上了obj,所以隐式绑定会把函数调用中的this绑定到这个上下文对象,也就是obj,所以this.a 和obj.a是一样的 但是,你需要记住一句话:this永远指向最后调用它的那个对象

function foo(){
    console.log(this.a);
}
var obj1 = {
    a:2,
    foo:foo
}
var obj2 = {
    a:3,
    obj1:obj1
}
obj2.obj1.foo();//2

上文代码执行结果是2,因为this指向最后调用它的那个对象,即obj1。

在使用隐式绑定的时候,有一个常见的问题,就是会出现隐式丢失

隐式丢失

隐式丢失是什么意思呢?其实就是被隐式绑定的函数丢失绑定对象。

function fn(){
	console.log(this.a)
}
var a = "hello"
var obj = {
	a: "obj",
	fn: fn
}
var demo = obj.fn; //函数别名
demo() //???

大家觉得这段代码最终控制台打印的是什么?
执行结果为,控制台打印了hello
为什么呢?
因为demo只是绑定了fn函数的引用,因此demo只是一个函数的调用,应用了默认绑定绑定到了全局对象,该情况很容易出现在回调函数上,例如:

function foo(){
    console.log(this.a);
}
function baz(fn){
    fn()
}
var obj = {
    a:2,
    foo:foo
}
var a = '我是全局对象的a';
baz(obj.foo);//我是全局对象的a
参数传递其实就是一种隐式赋值

还有一种比较特殊的情况就是定时器

function fn(){
	console.log(this.a)
}
var a = "hello"
var obj = {
	a: "obj",
	fn: fn
}
setTimeout(obj.fn,0); //hello

setTimeout第一个参数是传入回调函数,obj.fn被当做一个函数进行绑定,可以理解为:

function setTimeout(fn,delay){
	//等待delay毫秒后执行
	fn() //obj.fn
}

显示绑定

所谓显示,是因为你可以直接指定this的绑定对象,我们可以借助apply,call,bind等方法

apply和call作用一样,只是传参的方式不同,都会执行对应的函数,但是bind不一样,它不会执行,需要手动去调用。

function foo(){
 	console.log(this.a);
}
var obj = {
 	 a:2,
}

foo.call(obj);//2
foo.apply(obj);//2

但是,依然无法解决丢失绑定的问题,如下:

function sayHi(){
     console.log('Hello,', this.name);
 }
 var per = {
     name: 'mengyun',
     sayHi: sayHi
 }
 var name = 'anna';
 var Hi = function(fn) {
     fn();
 }
 Hi.call(per, per.sayHi); //Hello, anna
 

但是我们可以给fn也硬绑定this,就可以解决这个问题

function sayHi(){
    console.log('Hello,', this.name);
}
var per = {
    name: 'mengyun',
    sayHi: sayHi
}
var name = 'anna';
var Hi = function(fn) {
    fn.call(this);
}
Hi.call(per, per.sayHi); //Hello, mengyun

原因:因为per被绑定到Hi函数中的this上,fn又将这个对象绑定给了sayHi的函数。这时,sayHi中的this指向的就是per对象。

其实我更愿意用冒充来描述这个过程,Hi冒充per,拥有了per的一些方法和属性,然后传入的fn是per.sayHi这个函数如果不通过绑定的话还是默认指向了全局对象window所以输出‘anna',所以需要在进行一次冒充,把当前的方法冒充指向Hi,然后Hi指向的优势per,所以最后输出‘mengyun’。

上面代码用bind可以改写成:(bind在你不知道的js(上)中就是被归为硬绑定)

function sayHi(){
    console.log('Hello,', this.name);
}
var per = {
    name: 'mengyun',
    sayHi: sayHi
}
var name = 'anna';
var Hi = sayHi.bind(per);
Hi.call(per, per.sayHi); //Hello, mengyun

绑定例外

如果把null或者undefined作为绑定对象传入call,apply,bind,这些值往往会被忽略,实际应用的是默认绑定规则:

var obj= {
    name: 'mengyun'
}
var name = 'Anna';
function bar() {
    console.log(this.name);
}
bar.call(null); //Anna

new绑定

js跟其他语言不一样,没有类,所以我们可以用构造函数来模拟类 使用new来调用函数,会自动执行下面的操作:

创建一个全新的对象 这个对象会被执行[[Prototype]]连接 这个新对象会绑定到函数调用的this 如果函数没有返回其他对象,那么返回这个新对象,否则返回构造函数返回的对象

手写一个new

function _new(fn,...args){
  //1、创建一个空对象
  //2、这个对象的 __proto__ 指向 fn 这个构造函数的原型对象
  var obj = Object.create(fn.prototype);
  //3、改变this指向
  var res = fn.apply(obj,args);
  // 4. 如果构造函数返回的结果是引用数据类型,则返回运行后的结果,否则返回新创建的 obj
  if ((res!==null && typeof res == "object") || typeof res == "function") {
      return res;
  }
  return obj;
}

接下来我们用new来绑定一下this

function Person(name){
	 this.name = name;
}
var per = new Person('mengyun');
console.log(per.name);//mengyun

此时per已经被绑定到Person调用中的this上

优先级

我们先介绍前四种this绑定规则,那么问题来了,如果一个函数调用存在多种绑定方法,this最终指向谁呢?这里我们直接先上答案,this绑定优先级为:

显式绑定 > 隐式绑定 > 默认绑定

new绑定 > 隐式绑定 > 默认绑定

为什么显式绑定不和new绑定比较呢?因为不存在这种绑定同时生效的情景,如果同时写这两种代码会直接抛错,所以大家只用记住上面的规律即可。

function Fn(){
    this.name = '听风是风';
};
let obj = {
    name:'行星飞行'
}
let echo = new Fn().call(obj);//报错 call is not a function

那么我们结合几个例子来验证下上面的规律,首先是显式大于隐式:

//显式>隐式
let obj = {
    name:'行星飞行',
    fn:function () {
        console.log(this.name);
    }
};
obj1 = {
    name:'时间跳跃'
};
obj.fn.call(obj1);// 时间跳跃

其次是new绑定大于隐式:

//new>隐式
obj = {
    name: '时间跳跃',
    fn: function () {
        this.name = '听风是风';
    }
};
let echo = new obj.fn();
echo.name;//听风是风

箭头函数的this

ES6的箭头函数是另类的存在,为什么要单独说呢,这是因为箭头函数中的this不适用上面介绍的四种绑定规则。

准确来说,箭头函数中没有this,箭头函数的this指向取决于外层作用域中的this,外层作用域或函数的this指向谁,箭头函数中的this便指向谁。有点吃软饭的嫌疑,一点都不硬朗,我们来看个例子:

function fn() {
    return () => {
        console.log(this.name);
    };
}
let obj1 = {
    name: '听风是风'
};
let obj2 = {
    name: '时间跳跃'
};
let bar = fn.call(obj1); // fn this指向obj1
bar.call(obj2); //听风是风

为啥我们第一次绑定this并返回箭头函数后,再次改变this指向没生效呢?

前面说了,箭头函数的this取决于外层作用域的this,fn函数执行时this指向了obj1,所以箭头函数的this也指向obj1。除此之外,箭头函数this还有一个特性,那就是一旦箭头函数的this绑定成功,也无法被再次修改,有点硬绑定的意思。

当然,箭头函数的this也不是真的无法修改,我们知道箭头函数的this就像作用域继承一样从上层作用域找,因此我们可以修改外层函数this指向达到间接修改箭头函数this的目的。

function fn() {
    return () => {
        console.log(this.name);
    };
};
let obj1 = {
    name: '听风是风'
};
let obj2 = {
    name: '时间跳跃'
};
fn.call(obj1)(); // fn this指向obj1,箭头函数this也指向obj1
fn.call(obj2)(); //fn this 指向obj2,箭头函数this也指向obj2

总结

  • 那么到这里,对于this的五种绑定场景就全部介绍完毕了,如果你有结合例子练习下来,我相信你现在对于this的理解一定更上一层楼了。
  • 那么通过本文,我们知道默认绑定在严格模式与非严格模式下this指向会有所不同。
  • 我们知道了隐式绑定与隐式丢失的几种情况,并简单复习了作用域链与原型链的区别。
  • 相对隐式绑定改变的不可见,我们还介绍了显式绑定以及硬绑定,简单科普了call、apply与bind的区别,并提到当绑定指向为null或undefined时this会指向全局(非严格模式)。
  • 我们介绍了new绑定以及new一个函数会发生什么。
  • 最后我们了解了不太合群的箭头函数中的this绑定,了解到箭头函数的this由外层函数this指向决定,并有一旦绑定成功也无法再修改的特性。
  • 希望在面试题中遇到this的你不再有所畏惧,到这里,本文结束。