JavaScript:this指向和箭头函数

350 阅读4分钟

变量this的创建

结论:this的指向是动态的,取决于执行顺序而不是定义的作用域链。this指向的时调用它的对象中最近的那一个。


注意:本篇中的函数都是普通函数,没有使用到箭头函数,因为只有普通函数的执行环境中才有this这个变量被创建,才是指向离他最近的那一个

指向

一、全局对象

在函数中,this指向全局对象

function a() {
    console.log(this);        //window
    this.newvariable = 'hello';//改变全局对象的属性
}

var b = function() {
    console.log(this);       //window
}

a();
b();
console.log(newvariable);    //hello

a和b的调用,都是window对象,a()可以写成window.a();b()可以写成window.b();
无论是函数的statement还是expression,this都指向调用它们的window

二、上级对象

对象的方法属性中,this指向这个对象

var name = 'windowName
var c = {
    name: 'The c object',
    log: function() {
        this.name = 'Updated c object';//改变c的属性
        console.log(this.name);//c
    }
}

c.log(); // 'The c object'

log的调用实际上时window.c.log();this指向调用log的,离它最近的c对象

在setTimeout中不合预期

var c = {
  name: 'The c object',
  log: function () {
    console.log(this.name) //c
  },
}

c.log() // The c object
setTimeout(c.log, 1000) // windowName or undefined

this的指向收到执行环境栈的影响,在setTimeout中会有一个新的执行环境,所以在浏览器中指向window,在node中指向timeOut对象

解决方法: 加一个函数包装/绑定this

setTimeout(()=>{c.log()}, 1000);
setTimeout(function(){c.log()}, 1000)
setTimeout(c.log().bind(c), 1000)

三、没有调用它的对象时

对象方法属性的内部函数,this指向全局对象

var c = {
    name: 'The c object',
    log: function() {
        
        this.name = 'Updated c object';//改变对象c的属性
        console.log(this);
        
        var setname = function(newname) {
            this.name = newname;   //改变全局对象的属性
        }
        setname('Updated again! The c object');
        console.log(self);
    }
}
c.log();

log被c调用(c.log();),log中this指向c对象;
而setname没有调用它的对象(setname('Updated again! The c object');),setname中this指向window

这里很容易出错,所以经常会用到var self = this;
在进入内部函数之前确定下来this的指向,后面都用self代替this
this会有多个,但self只有一个,它指向c这个对象:

var c = {
    name: 'The c object',
    log: function() {
        var self = this;//self就是c
        
        self.name = 'Updated c object';
        console.log(self);
        
        var setname = function(newname) {
            self.name = newname;   //在当前执行环境中没能找到self对象,取outer environment中找,找到了,并确定下来就是c
        }
        setname('Updated again! The c object');
        console.log(self);
    }
}
c.log();

改变指向

用bind/apply/call改变

var person = {
    firstname: 'John',
    lastname: 'Doe',
    getFullName: function() {
        var fullname = this.firstname + ' ' + this.lastname;
        return fullname;
    }
}

var logName = function(lang1, lang2) {
    console.log('Logged: ' + this.getFullName());//Logged: John Doe
    console.log('Arguments: ' + lang1 + ' ' + lang2);//Arguments: en undefined
    console.log('-----------');
}

var logPersonName = logName.bind(person);
logPersonName('en');

logName.bind(person)将函数logName中this指向person对象,所以logPersonName('en')调用时this指向的不是window对象,而是person这个对象

bind的实际操作:
logName.bind(person)logName这个函数复制一份,将他作为一个方法,成为Person的一部分,logName函数中的this,就指向了Person

bind/apply/call是所有函数都有三个方法的(存在于函数的原型中)
bind是将函数绑到对象上,
applycall是将函数绑到对象上并执行这个函数,区别在于apply在对象后面的参数写成数组的形式(logName.apply(person,['en','em'])),call是分开写(logName.call(person,'en','em'))

箭头函数

函数在执行时会创建一个函数执行环境,普通函数的执行环境中会有this,arguments,而箭头函数没有。同时由于箭头函数没有this变量,就无法作为构造函数,也没有prototype对象。

没有this

箭头函数中的this永远指向定义时(不是执行时)作用域链最近的那个this,没有办法通过apply/bind/call直接修改箭头函数中this(因为根本就没有)但可以通过修改外层函数中this指向间接修改。

var id = 'Global';

function fun1() {
    // setTimeout中使用普通函数
    setTimeout(function fun3(){
        console.log(this.id);
    }, 2000);
}

function fun2() {
    // setTimeout中使用箭头函数
    setTimeout(() => {
        console.log(this.id);
    }, 2000)
}

fun1.call({id: 'Obj'});     // 'Global'

fun2.call({id: 'Obj'});     // 'Obj'

fun3作用域链的上一层是fun1,fun1.call({id: 'Obj'}); fun3有自己的this,所以即使修改了fun1中this的指向,但对fun3没有影响。定时器的作用使它是被全局/定时器调用的,this指向全局。

箭头函数的作用域链上一层是fun2,由于箭头函数没有this,它的this来自fun2,fun2.call({id: 'Obj'});修改了fun2中this的指向。

无法直接修改this

var id = 'Global';
// 箭头函数定义在全局作用域
let fun1 = () => {
   console.log(this.id)
};

fun1();     // 'Global'
// this的指向不会改变,永远指向Window对象
fun1.call({id: 'Obj'});     // 'Global'

不能作为构造函数使用

let Fun = (name, age) => {
    this.name = name;
    this.age = age;
};

// 报错
let p = new Fun('cao', 24);

没有arguments

arguments是一个类数组对象,不是数组

类数组对象:可以象数组一样有下标去访问属性,并且有length属性,但其proto是Object.prototype,不是Array.prototype 所以注意:

  1. arguments无法调用在Array.prototype上的方法,如concat
  2. concat函数可以接收多个参数,当参数是数组时,会拼接数组,当参数是非数组时,相当于把参数push,所以[].concat(arguments)得到的数组长度只有1
function Person(...destruction) {
 console.log(arguments) // Arguments(3) [1, 2, 3, callee: (...), Symbol(Symbol.iterator): ƒ]
 let array = []
 console.log(array.concat([4, 5, 6])) // [4, 5, 6]
 console.log(array.concat(4)) // [4]
 console.log(array.concat(4, 5)) // [4, 5]
 console.log(array.concat(arguments)) // [Arguments(3)] !!!!!
 console.log(array.concat(destruction)) // [1, 2, 3]
}
Person(1, 2, 3)
const ArrowPerson = () => {
  console.log(arguments) // undefined
}
function Person() {
  console.log(arguments) // Arguments(2) ["1", "2", callee: ƒ, Symbol(Symbol.iterator): ƒ]
}
ArrowPerson('1', '2')
Person('1', '2')