js 中的 this 小结

@一棵菜菜  June 20, 2019

this相关内容总结,比如如何正确判断 this?箭头函数的 this 是什么?


流程图【必须牢记】

this.png

图中的流程只针对于单个规则。

小结

  1. 首先,new的方式优先级最高,——var f = new foo(),foo中的this指向f实例
  2. 接下来是 fn.bind() 这些函数, ——fn 中的 this 永远由第一次 bind 决定,如果bind第一个参数为空,那么就是 window
  3. 然后是 obj.foo() 这种调用方式, —— foo中的this指向obj
  4. 最后是 foo() 这种直接调用方式, ——不管 foo 函数被放在了什么地方,this 一定是 window
  5. 同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。 ——箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this
  6. 立即执行函数表达式、setTimeout计时器等的回调函数若为普通函数,则函数中的this指向 window【需要查看书籍确认下】。若为箭头函数,则按箭头函数的规则来。

1. 函数调用

练习3-1

function foo() {
  console.log(this.a)
}
var a = 1
foo()

const obj = {
  a: 2,
  foo: foo
}
obj.foo()

const c = new foo()
分析:
对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
对于 obj.foo() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 foo 函数中的 this 就是 obj 对象;
对于 new 的方式来说,this 被永远绑定在了 c 上面,不会被任何方式改变 this。

练习3-2【好例子,interview】

var num = 1;

function foo() {
  console.log(num,this.num);
  console.log(cc)
}

function biz() {
  var num = 2;
  var cc = 3;
  foo();
}

biz();
答案:1 1;报错:Uncaught ReferenceError: cc is not defined
我的分析:
由于foo函数是在biz函数内部直接被调用的,对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window,访问的也是全局环境,所以this.num值为1。

词法作用域链:

window
  |—— num
  |—— foo()
  |—— biz() 
    |—— num
    |—— cc 

所以:foo()执行时访问到的 num 其实是全局的num=1 访问的cc也是全局的,但是由于cc并未在全局环境声明,所以报错。注意:foo是无法访问到biz中声明的变量的!


2. 箭头函数【典例】

  1. 箭头函数里其实是没有 thisarguments 的,箭头函数中的 this 只取决包裹箭头函数的 第一个普通函数 的this。—— 箭头函数里的this由它被声明时所在的作用域控制(即指向父作用域)【牢记】
  2. 由于this已被提前直接绑定了,所以,无法用 call()apply()bind() 去改变 this 的指向的。即传入的第一个参数被忽略。
  3. 如果使用箭头函数,以前的那种hack写法:var that = this;就不再需要了。
箭头函数适合于无复杂逻辑或者无副作用的纯函数场景下,例如:用在 map、reduce、filter 的回调函数定义中。

例子1:

function a() {
  return () => {
    return () => {
      console.log(this)
    }
  }
}

const obj = {
  num: 2,
  a: a
}

// 执行1
a()()();
// 执行2
obj.a()()();
分析:
在这个例子中,因为包裹箭头函数的第一个普通函数是 a(或者理解为:箭头函数最终是在函数a中被定义的,即父作用域是函数a),所以箭头函数中的this等于函数a中的this。而a中的this指向 由a如何被调用控制。
执行1:普通函数a是直接被执行的a(),所以此时普通函数a中的 this 是 window,所以输出结果为window。
执行2:普通函数a是被obj.a()的方式调用的,所以此时普通函数a中的 this 是 obj,所以输出结果为obj。

例子2:

var name = 'cyc';
var test = {name:"shitou"};
var fun = () => {this.name};
fun.apply(test);
结果:cyc
对箭头函数使用apply、call、bind都是无法绑定this的。fun里的this指向了window。

例子3:

普通函数fn被直接调用(this直接指向window):

var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = function () {
            return new Date().getFullYear() - this.birth; // this指向window或undefined
        };
        return fn();
    }
};
obj.getAge(); // NaN

箭头函数fn被调用:【典例】

var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
        return fn();
    }
};
obj.getAge(); // 30  写这个代码时 new Date().getFullYear()返回的是2020哈
箭头函数fn在getAge函数中被定义,所以this指向getAge中的作用域。

非常推荐廖雪峰的文章《箭头函数》


箭头函数不适用的场景【一定要了解】

以下6种情况是不能使用箭头函数的:

1. 不适合定义对象字面量方法

箭头函数不适合定义对象的方法(对象字面量方法、对象原型方法、构造器方法),因为箭头函数没有自己的 this,其内部的 this 指向的是外层作用域的 this

const obj = {
  num: 2,
  a: () => console.log(this.bar) 
}

// 另一种写法
var a =()=> console.log(this.num);
const obj = {
  num: 2,
  a: a
}

obj.a()
undefined
箭头函数a是在全局作用域下被定义的,所以a中的this指向window。
this 并不是指向 json 这个对象,而是再往上到达全局作用域。块级作用域只有函数、let、const等才能创建哦。

2. 不适合构造函数

作为构造函数,一个方法需要绑定到对象。所以构造函数不能使用箭头函数的写法:

var Person =(name)=>{
    this.name = name;
};
var p = new Person(); // 报错 Person is not a constructor
// 不可以当作构造函数,也就是说,不可以使用 new 命令

当我们用new生成一个Person的实例的时候其实要进行四个步骤:

  1. 生成一个新的对象
  2. 把构造函数里this的值指向新生成的对象
  3. 把这个对象绑定到它的原型对象
  4. 返回这个新生成的对象

但是我们通过这个箭头函数并没有实现把这个this值绑定到我们新生成的p对象上去,所以并不会得到我们想要的效果,那么这个时候我们只能使用我们的普通函数。

3. 不适合对象原型prototype

prototype中的函数不能使用箭头函数的写法:

function Person(name){
    this.name = name;
}
Person.prototype.getName = ()=> console.log(this.name);;

const p = new Person();
p.getName();// undefined
this 并不是指向 p,根据变量查找规则,回溯到了全局作用域

4. 不适合真的需要this

箭头函数不适合定义结合动态上下文的回调函数(事件绑定函数),因为箭头函数在声明的时候会绑定静态上下文

const button = document.querySelector('button');
button.addEventListener('click', () => {  
    this.textContent = 'Loading...';
});
// this 并不是指向预期的 button 元素,而是 window

5. 不适合需要arguments类数组对象时

在箭头函数当中是没有arguments这个对象的:

var Person =name=>console.log(arguments)
Person('caicai');// 报错:arguments is not defined

6. 多层函数嵌套时不建议使用箭头函数

箭头函数的亮点是简洁,有多层函数嵌套的情况下,箭头函数反而影响了函数的作用范围的识别度,这种情况不建议使用箭头函数。


3. bind

最后种情况也就是 bind 这些改变上下文的 API 了,对于这些函数来说,this 取决于第一个参数,如果第一个参数为空,那么就是 window

let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => ?
不管我们给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定,所以结果永远是 window
可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定 this 最终指向哪里。

4. 立即执行函数表达式

var myobject = {
  foo: "bar",
  func: function () {
    var self = this;
    console.log(this.foo);
    console.log(self.foo);

    var test = (function () { // 普通函数的写法。若这里是箭头函数,则this值和self值相同
      console.log(this.foo);
      console.log(self.foo);
    })();
  },
};
myobject.func();
答案:bar bar undefined bar
“匿名函数的执行环境具有全局性”,所以最里层那个test函数中this指向全局环境,全局环境没有定义foo变量所以输出undefined。在匿名函数外部将this保存到一个内部函数可以访问的变量self中,可以通过self访问这个对象,所以self.foo为bar.

5. 计时器

setTimeout的回调函数为普通函数:

function foo(){
    console.log(this.a)
    setTimeout(function(){
        console.log(this.a,this)
    },0)
}

var oo = {a:1,foo:foo};
oo.foo();
答案: 1;undefined window
计时器回调函数若为普通函数,则函数中的this指向window

setTimeout的回调函数为箭头函数:

function foo(){
    console.log(this.a)
    setTimeout(()=>{
        console.log(this.a,this)
    },0)
}

var oo = {a:1,foo:foo};
oo.foo();
答案: 1;1 oo
计时器回调函数若为箭头函数,则函数中的this指向foo函数中的this,foo函数时被oo.foo()方式调用的,所以this指向oo。

聚美面试题【典例】

var name = "caicai";
var sheep = {
  name: "yangyang"
};
sheep.getName = function() {
  console.log(this.name);
};

sheep.getName(); //(1)

window.setTimeout(sheep.getName, 1000);(2) 聚美的原题

window.setTimeout(function(){
 sheep.getName(); // (3)
}, 1000);

window.setTimeout(()=>{
 sheep.getName(); // (4)
}, 1000);

答案

  1. 'yangyang','caicai','yangyang','yangyang'

注意:
(1)行是函数被调用(对象.的方式);
(2)行因为setTimeout的回调函数中 this = window。 因为只是传递了sheep.getName这个函数给setTimeout作为回调,而不是执行!执行时在setTimeout函数内部被执行的!


其他练习

练习1-1

            var myObject = {
                num: 2,
                add: function () {
                    this.num = 3;
                    (function () {
                        console.log(this.num);
                        this.num = 4;
                    })();

                    console.log(this.num);

                }
            }

            myObject.add();

答案:
undefined
3

解析:立即执行函数表达式中(这里也是闭包)中的this实际指向的是window(函数没有指明调用者呢,那就让this指向全局对象吧),但window未查找到有num属性,所以是undefined。(如果有提前声明window.num=10,那么这里就会输出10而不是undefined啦)

练习2-1

var obj1 = {
        obj2: {
          obj3: function () {
            console.log(this);
          }
        }
      };
      console.log(obj1.obj2.obj3()); 
答案:{obj3: ƒ},其实obj1.obj2.obj3()这时候的this 就等于 obj1.obj2。

练习2-2

var obj={
    a:2,
    b:{
        a:3,
        c:function(){
            this.a = 4;
            return this.a;
    }
    }
}

console.log(obj.a);
console.log(obj.b.a);
console.log(obj.b.c()); // 这时c函数里的this即指代obj.b
console.log(obj.b.a);

答案:
2 3 4 4


添加新评论