关于this你需要知道的

478 阅读12分钟

image.png

this是什么

在函数调用的时候,会创建一个活动对象(active object,有时也称为执行上下文)。这个记录会包括函数在哪里被调用(调用栈)函数的调用方法传入的参数等信息this就是活动对象记录在其中的一个属性,会在函数函数执行的过程中用到。

this与作用域的关系

this在任何时候都不指向词法作用域,作用域确实与对象类似,可见的标识符都是它的属性,但是作用域对象无法通过js代码访问,它存在于js引擎内部。 来个栗子:

function foo(){
    var a = 2;
    this.bar();
}
function bar() {
    console.log(this.a);
}
foo();

注意: this与词法作用域查找混合使用时,这是无法实现的。 this在函数调用的时候发生绑定,它指向什么完全取决于函数在哪里被调用。

this解决了什么问题

  1. this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计 得更加简洁并且易于复用。

来个栗子说明一下:

function identify(){
    return this.name.toUpperCase();
}
function speak() {
    var greeting = "Hello, I'm " + identidy.call(this);
}
var me = {
    name:"Rose",
}
var you = {
    name:"Jack"
}
identify.call(me);
identify.call(you);

speak.call(me);
speak.call(you);

如果不使用this呢?

function identify(context){
    return context.name.toUpperCase();
}
function speak(context) {
    var greeting = "Hello, I'm " + identidy(context)
}
identify(you) ; Jack
speak(me); //Hello, I'm Rose

大白话就是:使用this可以让不具备某个方法或属性的对象,访问该属性或调用该方法

this取决于什么?

  1. this在函数调用的时候发生绑定,指向完全取决于函数在哪里执行
  2. 始终坚持一个原理:this 永远指向最后调用它的那个对象this 永远指向最后调用它的那个对象this 永远指向最后调用它的那个对象重要的事情说三遍。

来个测试题看看你对this了解情况: 栗子1:

var number = 5;
var obj = {
    number: 3,
    fn1: (function () {
        var number;
        this.number *= 2;
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    })()
}
var fn1 = obj.fn1;
fn1.call(null);
obj.fn1();
console.log(window.number);

栗子1分析:

1. 声明的obj里面的fn1有自调用函数会先自执行,this.number *= 2; 
   即为window.number *= 2; 此时 window.number = 10; 自调用申明了一个变量number值为undefined,
   nudefined *2 ,还是undefined, 最后为自调用函数内部的number赋值为3.
2. fn1引用的实际上是fn1返回的匿名函数,fn1.call(null),指的是匿名函数里面的thiswindow,
    执行 var num = this.number; 即为 num = window.number ,所以 `输出为10`,this.number *= 2; 
    此时window.number *= 2; window.number = 20;
3. 匿名函数内部并没有申明number,需要往上层作用域找,上层作用域 number = 3; 执行完 number * 3,
    此时将上层作用域的number赋值为9`输出为9`
4. 自调用函数内申明的number=9;
5. obj.fn1() , 将匿名函数内部的this隐式转为obj, var num = this.number; 即为num = obj.number = 3; 
   `输出3` ,number *= 3; 由于匿名函数内部没有number,去上层作用域找,number = 9;number *= 3;
   所以`输出 27`,
6. 此时window.number = 20; 

误解指向自身

栗子2:

function countor(num){
    console.log('countor',num)
    //记录foo被调用的次数
    this.count ++;
}
countor.count = 0;
var i = 0;
for(i = 0; i < 10 ; i++) {
    if(i > 5) {
        countor(i)
    }
}
// foo:6 
// foo:7
// foo:8 
// foo:9
console.log(countor.count)
  • 为什么countor明明调用了4次但是值还是0?

  • 如果希望countor可以具备记录函数的调用次数有哪些解决方案? 方法一:

function countor(num){
    console.log('countor',num);
    countor.count ++; 
}
countor.count = 0;
for(i = 0 ;i < 10; i++){
    if(i > 5 ){
        countor(i)
    }
}
console.log(countor.count)

方法二:

function countor(num){
    console.log('countor',num);
    data.count ++;
}
var data = {
    count: 0
}
var i ;
for(i = 0 ;i < 10; i++){
    if(i > 5 ){
        countor(i)
    }
}
console.log(data.count)

虽然解决了问题,但是避开了this

通过直接将 countor.count的执行上下文直接指向当前的函数即 直接调用 countor.count 替代this,但还是避开了this

方案三:

function countor() {
    console.log('countor',num)
    //记录foo被调用的次数
    this.count ++;
}
countor.count = 0;
var i = 0;
for(i = 0; i < 10 ; i++) {
    if(i > 5) {
        //直接改变函数调用时的上下文,将this指向countor本身
        countor.bind(countor,i)
    }
}

上面的案例说明了一个误区;

  • this在任何情况下都不指向函数的词法作用域

现在我们来回答为什么countor明明调用了4次但是值还是0?

    1. 从上面的3个案例可以看出,全局函数的this执行上下文不是指向函数本身而是window
    1. 可以通过改变函数调用时的this指向来改变函数执行上下文

this调用位置,当前执行上下文的栈顶

image.png

根据不同的调用位置,确定使用下面4种绑定规则的哪一种:

绑定规则

默认绑定、隐式绑定、显式绑定、new绑定

默认绑定

全局方法默认绑定为window,严格模式,不能将全局对象用于默认绑定,因此this会绑定到undefined

1.1 题目一
function foo() {
    console.log(this.a);
}
var a = 2;
1.2 题目二
function foo(){
    'use strict';
    console.log(this.a);
}
var a =2;
foo(); 

严格模式下会报错; TypeError: Cannot read property 'a' of undefined

如果改为调用的地方为'use strict'呢?

function foo() {
    console.log(this.a);
}
var a = 2;
(function(){
    'use strict';
    foo()
})()

结论:默认绑定(非严格模式下this指向全局对象, 严格模式下this会绑定到undefined),注意是申明的时候,而非调用的时候

隐式绑定

当函数引用上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象

2.1 题目一
function foo() {
    console.log(this.a);
}
var obj = {
    a:2,
    foo:foo
}
obj.foo(); 
2.1 题目二

如果引用2层呢?

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

隐式丢失:常见在回调函数中

为什么隐式绑定的this会丢失?

3.1 题目一
function foo() {
    console.log(this.a);
}
var obj = {
    a:2,
    foo:foo
}
var bar = obj.foo;
var a = "global";
bar(); 

由于obj.foo引用的是函数本身,bar()调用的位置在全局,所以this.a输出 global

下面这个栗子更微妙、更出乎意料

3.2 题目二
function foo() {
    console.log(this.a);
}
function doFoo(fn){
    fn();
}
var obj = {
    a:'local',
    foo:foo
}
var a = "global";
doFoo(obj.foo)
3.3 题目三

现在我们不用window调用doFoo,而是放在对象obj2里,用obj2调用:

function foo(){
    console.log(this.a);
}
function doFoo(fn){
    console.log(this);
    fn();
}
var obj = {a:"local",foo};
var a = "global";
var obj2 = {a:"obj2", doFoo};

obj2.doFoo(obj.foo);

是不是感觉so easy,再来一个有挑战的:

3.4 题目四
var length = 10;
function fn () {
    console.log(this.length);
}
var obj = {
    length: 5,
    method: function (fn) {
        console.log(this.length)
        fn();
        arguments[0]();
    }
};
obj.method(fn, 1);

为什么arguments[0]()的值是2呢?

显示绑定

  • 通过call()或者apply()bind()方法直接指定this的绑定对象, 如foo.call(obj)

使用显示绑定有三点需要注意:

  1. 使用 .call()、apply() 的函数会直接执行
  2. 使用 .bind() 会创建一个新的函数,需要手动调用才能执行
  3. .call() 、.apply()用法基本类似,不过call接受若干个参数,apply接受一个数组。

写个伪代码实现看看bind是如何实现的:

Function.prototype.call = function (context, ...args ){
    const fn = Symbol();
    context.fn = this;
    const result = context.fn(...args);
    delete context.fn;
    return result;
}

bind做了4件事:

  1. 在执行上下文上增加一个唯一属性fn
  2. 将this指向该属性
  3. 执行fn函数
  4. 删除上下文属性fn,并返回fn调用的结果
4.1 题目一
function foo() {
    console.log(this.a);
}
var obj = {a: 1};
var a = 2;
foo();
foo.call(obj);
foo.apply(obj);
foo.bind(obj);

foo.bind(obj),原因是因为bind创建了一个新函数需要用变量接收并调用,因此此处不会执行。

注意:如果call、apply、bind接收到的第一个参数是空或者null、undefined的话,则会忽略这个参数

function foo() {
    console.log(this.a);
}
var a = "global";
foo.call();
foo.call(null);
foo.call(undefined);

知道显示绑定后,我们来看一个它的妙用

4.2 题目二
var obj1 = {
    a:1
}
var obj2 = {
    a:2,
    foo1:function(){
        console.log(this.a);
    },
    foo2:function() {
        console.log(this);
        setTimeout( function (){
            console.log(this);
        },0)
    }
}
var a = "global";
obj2.foo1();
obj2.foo2();
如果希望定时器里this指向 obj1 如何改造?
4.3 题目三
setTimeout(function(){
    console.log(this);
}.call(obj1),0)

// 只需要在上例的回调函里面 .bind(obj1)

所以有小伙伴就会问了,我下面的这种写法不可以吗?

obj2.foo2.bind(obj1)

注意⚠️:这种写法实际上是改变了foo2函数内部的this, 而 setTimeout里的函数thisfoo2函数里面的this是没有关系的,定时器调用里面的this始终都是window

4.4 题目四

OK👌,我们不用定时器,把它干掉,换成一个函数:

var obj1 = {
    a:"obj1"
}
var obj2 = {
    a:"obj2",
    foo1:function() {
        console.log(this.a);
    },
    foo2:function() {
        console.log(this.a);
        function inner() {            
            console.log(this.a);
        }
        inner()
    }
}
var a = 3;
obj2.foo1();
obj2.foo2()

如果将 inner() 改为显示绑定呢?

inner.call(obj1)
4.5 题目五

看看下题会输出什么?

function foo() {
    console.log(this.a);
}
var obj = {
    a:1
}
var a = "global";
foo();
foo.call(obj);
foo().call(obj)

注意⚠️:此处会报错Uncaught TypeError: Cannot read property 'call' of undefined,因为 call必须要被函数调用。

那如果函数foo里面返回一个函数呢?

4.6 题目六
function foo() {
    console.log(this.a);
    return function() {
        console.log(this.a)
    }
}
var obj = {
    a:1
}
var a = "global";
foo();
foo.call(obj);
foo().call(obj);

如果把上面的call换成bind会如何?

4.7 题目七
function foo() {
    console.log(this.a);
    return function () {
        console.log(this.a)
    }
}
var obj = {
    a:1
}
var a = "global";
foo();
foo.bind(obj);
foo().bind(obj)

注意⚠️:foo.bind(obj) 不会执行,因为返回的新的函数需要变量接收并调用才可以。

4.8 题目八

函数内层的this与函数外层的this有关系?内层this到底指向谁?我们重要的口诀再来一遍:this由最后一个调用它的对象决定

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

foo.call(obj)()

如果将上面的函数返回函数放入对象中呢?

4.9 题目九
var obj = {
    a:"obj",
    foo:function() {
        console.log('foo',this.a);
        return function() {
            console.log('inner',this.a)
        }
    }
}
var obj2 = { a: "obj2"};
var a = 2
obj.foo();
obj.foo.call(obj2)();
obj.foo().call(obj2)

加个参数玩玩

4.10 题目十
var obj = {
    a:1,
    foo:function(b) {
        b = b || this.a;
        return function(c) {
            console.log(this.a + b + c)
        }
    }
}
var a = 2;
var obj2 = { a: 3};
obj.foo(a).call(obj2,1);
obj.foo.call(obj2)(1)

new绑定

new做了什么?

function myNew (Person) {
    const context = Object.create(Person);
    let result = context.call(context);
    if((typeof result !== 'null') && (typeof result === 'object' || typeof result === 'function')) {
        return result;
    }else {
        return context;
    }
}

总结4句话:

  • new一个新对象,
  • 新对象原型指向构造函数
  • 并将this指向创建的新对象
  • 函数如果没有返回其他对象,则返回新创建的对象,如果构造函数返回的为对象或者函数,则将该对象或函数返回
new绑定的优先级问题
5.1 题目一
function foo() {
    console.log(this.a);
}
var obj1 = {
    a:2,
    foo:foo
}
var obj2 = {
    a:3,
    foo:foo
}
obj1.foo.call(obj2);
obj2.foo.call(obj1);

可以看到显示绑定优先级更高

下面看看new绑定隐式绑定优先级

5.2 题目二
function foo(something){
    this.a = something;
}
var obj1 = {
    foo:foo
}
var obj2 = {};

obj1.foo(1);
console.log(obj1.a);

obj1.foo.call(obj2,3);
console.log(obj2.a);

var bar = new obj1.foo(4);
console.log(obj1.a);
console.log(bar.a);

可以看出new绑定比隐式绑定优先级更高,现在我们看看 new绑定与显示绑定哪个优先级更高??

5.3 题目三

由于我们无法 通过 new foo.call(obj1)测试,所以我们间接通过硬绑定实现

function foo(something){
    this.a = something;
}
var obj1 = {};
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a);

var baz = new bar(3);
console.log(baz.a);
console.log(bar.a);

new bar并没有像我们预计的那样把obj1.a修改为3. bind的内部实现:会判断是否被new 调用,如果是的话就会使用新创建的this替换硬绑定的this

5.4 题目四
var name = 'window'
function Person (name) {
  this.name = name
  this.foo = function () {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo.call(person2)()
person1.foo().call(person2)

解题思路:

person1.foo().call(person2)可以理解为 person1.foo()调用返回的匿
名函数的上下文绑定到person2身上

总结,判断this:

根据优先级来判断函数在某个调用位置应该用的是哪条规则,可以按照下面的顺序来判断

  • 函数是否在new中调用(new绑定)?如果是this绑定就是新创建的对象。var bar = new foo():

  • 函数是通过call、apply、(显示绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象。

  • 函数是否在某个上下文中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。var bar = obj1.foo();

  • 如果都不是的话,使用默认绑定,在非严格模式下绑定到undefined,否则绑定到全局对象 var var = foo();

箭头函数

  • 对于上面的问题,this永远指向最后一个绑定它的对象,但是对于箭头函数就不一样了。

  • 箭头函数的this 由外层的作用域决定的,指向定义时的this,而非执行时。

它里面的this由外层的作用域决定的是什么意思呢?

箭头函数中没有this绑定,必须通过查找作用域链来决定它的值,如果箭头函数被非箭头函数包含,则this就指向最近的一层非箭头函数的this, 否则this为undefined

6.1 题目一

来个简单的栗子:

var name = 'window'
var obj1 = {
    name: 'obj1',
    foo: function () {
         console.log(this.name)
    }
}

var obj2 = {
    name: 'obj2',
    foo: () => {
          console.log(this.name)
    }
}
obj1.foo()
obj2.foo()

6.2 题目2
var obj = {
    name:"obj",
    foo1:() => {
        console.log(this.name);
    },
    foo2:function (){
        console.log(this.name);
        return () => {
            console.log(this.name);
        }
    }
}
var name = "galbol";
obj.foo1();
obj.foo2()();

解题:

  • 对于obj.foo1()函数的调用,它的外层作用域是window,对象obj当然不属于作用域,(作用域只有全局作用域与函数创建的局部作用域),所以输出为 galbol
6.3 题目3
var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  }
  this.foo2 = () => {
    console.log(this.name)
  }
}
var person2 = {
  name: 'person2',
  foo2: () => {
    console.log(this.name)
  }
}
var person1 = new Person('person1')
person1.foo1()
person1.foo2()
person2.foo2()

解题:

1. person1.foo2() 构造函数里面的箭头函数,箭头函数`this由外层作用域决定,且指向定义时而非执行时`,
   外层作用域是函数Person,且构造函数new 生成了新对象,所以此时this指向为person1
6.4 题目四
var name = 'window'
var obj1 = {
  name: 'obj1',
  foo1: function () {
    console.log(this.name)
    return () => {
      console.log(this.name)
    }
  },
  foo2: () => {
    console.log(this.name)
    return function () {
      console.log(this.name)
    }
  }
}
var obj2 = {
  name: 'obj2'
}
obj1.foo1.call(obj2)()
obj1.foo2().call(obj2)

总结一下箭头函数需要注意的点吧:

  • 它里面的this由离它最近的外层函数来决定,且指向函数定义时的this而非执行时

  • 字面量创建的对象,作用域是window,如果里面有箭头函数是属性的话,this指向的是window

  • 构造函数创建的对象,作用域可以理解为这个构造函数,且这个构造函数this是指向新创建的对象

  • 箭头函数里面的this是无法通过bind、apply、call来修改的,但是可以通过改变作用域中的this指向来间接修改。

优点:

  • 箭头函数可以让代码拥有更加简洁的语法
  • this由外层作用域决定,所以可以避免写 const that = this;这样的代码

需要避免使用箭头函数的场景

  1. 使用箭头函数定义对象的方法
let obj  =  {
    value:"Fancy",
    getValue:() => console.log(this.value);
}
obj.getValue() //undefined
  1. 定义原型方法
function Foo(value) {
    this.value = value;
}
Foo.prototype.getValue = () => console.log(this.value);
const foo1 = new Foo(1);
foo1.getValue(); //undefined;
  1. 作为事件的回调函数
const button = document.getElementsById("myButton");
button.addEventListener("click",() =>{
    console.log(this === window) ; //true
    this.innerHTML = "Clicked button";
})

4.构造函数使用箭头函

const Foo = (value) => {
    this.value = value;
}
const foo1 = new Foo(1);
console.log(foo1); // Foo is not a constructor

最后来综合题:

var name = 'window'
function Person (name) {
  this.name = name
  this.foo1 = function () {
    console.log(this.name)
  },
  this.foo2 = () => console.log(this.name),
  this.foo3 = function () {
    return function () {
      console.log(this.name)
    }
  },
  this.foo4 = function () {
    return () => {
      console.log(this.name)
    }
  }
}
var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1()
person1.foo1.call(person2)

person1.foo2()
person1.foo2.call(person2)

person1.foo3()()
person1.foo3.call(person2)()
person1.foo3().call(person2)

person1.foo4()()
person1.foo4.call(person2)()
person1.foo4().call(person2)

解题思路;

person1.foo2()
person1.foo2.call(person2)
// 需要注意,在构造函数中,this.foo2 = () => console.log(this.name);
// this由外层作用域决定,且指向函数定义而非执行时,这里的外层作用域是函数Person,且是构造函数,new生了
// person1,所以此时this指向为person1

参考阅读

《你不知道的js二部分》

40题搞懂this