Javascript中this关键字理解,你懂了?

其实这个问题在面试中基本都会问到,我想大多数童鞋们都经历过吧,你是否答对了呢?

this指向哪里呢?

在Javascript中,和大多数面向对象的编程语言一样,这是一个特殊的关键字,它在方法中用于引用
被调用方法的对象。

In JavaScript, as in most object-oriented programming languages, this is a special keyword that is used within methods to refer to the object on which a method is being invoked.
——jQuery Fundamentals (Chapter 2), by Rebecca Murphey

值得注意,this在javascript中和执行环境有关,而非声明环境。

The this keyword is relative to the execution context, not the declaration context.

首先,要搞清楚js里函数的几种调用方式:

  • 普通函数
  • 作为方法来调用
  • 作为构造函数来调用
  • 使用call/apply方法来调用
  • Function.prototype.bind方法
  • ES6箭头函数

以上6中调用方式,不管函数式按照哪种方式来调用,只要记住一个要点:谁调用这个函数或方法,this就指向谁

接下来具体分析每种情况:

1.普通函数调用

function foo(){
    this.name = 'hank';
    console.log(this); // node=> global 浏览器=> window
    console.log(this.name); //hank
}
foo();  
console.log(global.hasOwnProperty(foo())); // node输出: true

上面的代码中foo函数作为一个普通函数调用,实际上foo是作为全局对象window的一个方法来进行调用, ,浏览器上如:window.foo();所以这个地方是window对象调用了foo方法,那么foo函数当中的this即指向window,同时window还有了另外一个属性name,值为hank`。

Node.js 中的全局对象是 global,所有全局变量(除了 global 本身以外)都是 global 对象的属性。在 Node.js 我们可以直接访问到 global 的属性,而不需要在应用中包含它。

var name = 'hank';
function foo(){
    console.log(this.name); // 浏览器=> hank   node => undefined       }
foo();

同样上面的foo作为window的方法来调用,在代码的一开始定义了一个全局变量name,值为hank,它相当于window的一个属性,即window.name='hank',因为在调用的时候this是指向window的,因此这里的输出的结果是hank

但是,为什么node.js中this无法获取到name呢?因为node中文件都是模块化的,每个文件内定义的变量可以说是局部变量(只在该文件中能访问到)。如果要定义全局变量,可以不用var关键字或global.name=’hank’,若要其它文件能访问则导出模块中 exports.name = ‘hank’。nodejs测试例子:

name = 'hank';   // 全局变量                               
var age =10;      //局部变量                             
global.sex = 'man';       // 全局变量                       
function foo(){                                
    console.log(this===global); //true         
    console.log(this===module);  //false       
    console.log(this.name);  //hank            
    console.log(this.age); // undefined        
    console.log(this.sex) ;  //man             
}                                              
foo();                                         
console.log('---------------------------');    
console.log(this===exports); //true            
console.log(this===global); //false    并没有绑定到global 而是绑定到了exports         
console.log(this.name) ;// undefined           
console.log(global.name); // hank              
console.log(this.age); // undefine             
console.log(this.sex) ;  //undefine            

2.作为方法来调用

上面的代码中,普通函数的调用是作为window对象的方法或者global全局对象的属性来进行调用,显然this是指向了windowglobal对象。

接下来看其他形式:

var name = 'hank';                                                                                                                                                                                                                             
var foo = {                                                                                                                                                                                                                                     
    name:'HANK',                                                                                                                                                                                                                                
    showName:function () {                                                                                                                                                                                                                      
      console.log(this===global); // false  ---  true                                                                                                                                                                                           
      console.log(this===exports);// false  ---  false                                                                                                                                                                                          
      console.log(this===foo); //    true   ---  false                                                                                                                                                                                          
      console.log(this.name);  // HANK  ----- window=> hank  node=> undefined                                                                                                                                                                   
    }                                                                                                                                                                                                                                           

} ;  
foo.showName(); // HANK                                                                                                                                                                                                                         
// 这里是foo对象调用了showName方法,显然这里this指向foo对象的,所以会输出foo对象内部属性name 
var sn = foo.showName;                                                                                                                                                                                                                          
sn();  // window => hank  node=>undefined  
/*这里将foo.showName方法赋值给sn变量,此时sn变量相当于window对象的一个属性,因此showName()执行的时候相当于window.showName(),即window对象调用sn这个方法 ,所以this 执向window。虽然node.js中this虽然指向了global,但是var name='hank'在node中确实局部变量,所以结果是undefined。
*/  

换一种方式写:

var fooA = {
    name:'hank',
    showName:function(){
        console.log(this.name);
    }
}
var fooB = {
    name:'HANK',
    getName:fooA.showName
}
fooB .getName();  // 输出 HANK

上面代码中,虽然showName方法是在fooA这个对象中定义的,但是调用的时候却是在fooB这个对象中调用,因此this对象指向fooB

3.用作构造函数来调用

function Foo(name){                                                            
    this.name = name;                                                           
  console.log(this); // 没有new:window/global   有new: Foo { name: 'hank' }                                                          
 }                                                                              
 var nf =  Foo('hank');                                                         
 console.log(nf.name);  // 浏览器上输出: undefined  node中nf=> undefined 结果报错          
console.log(window.name); //  hank                                             
 /* 这里没有进行new操作,相当于window对象调用Foo('hank')方法,那么this指向window对象,                    
    并进行赋值操作window.name='hank'.                                                  
  */                                                                            
 console.log(global.name); // hank    这里this指向global,this.name 相当于global.name   
3.1 new 操作符

上面的代码用new操作符,结果如何呢?

var nfb = new Foo('hank');  // 这里的 this指向new处理的那个对象
console.log(nfb.name);  // 结果输出: hank
console.log(global.name); // node中输出: undefined 

我们来模拟一下new操作符的内部过程

function foo(name){
    var obj = {};
    obj.__proto__ = Foo.prototype; //原型继承, obj的__proto__指向了Foo.prototype         Foo.call(obj,name);
    return obj;
}
var f = foo('hank');
console.log(f.name); //输出 hank
  • 这上面foo里,首先创建了一个空对象obj,将obj__proto__指向foo.prototype完成对象原型的属性和方法的继承。
  • foo.call(obj,name)这里函数foo作为apply/call调用(见下面),将foo对象里的this改为obj,完成了obj.name = name操作。
  • 返回对象obj

    因此foo('hank')返回了一个继承了foo.prototype对象上的属性和方法,以及拥有name属性为hank的对象,并将它赋值给变量f,所以console.log(f.name)输出 hank。

4.apply/call 方法的调用 ——————最大作用:就是改变this指向

在javascript里函数也是对象,因此函数也有方法,从Function.prototype上继承到Function.prototype.apply / Function.prototype.call方法。
apply/call方法最大的作用就是能改变this关键字的指向。
Obj.method.apply(AnotherObj,argments);

var name = 'hank';
//对象子面量
var Foo = {
    name:'HANK',
    showName:function(){
        console.log(this.name);
    }    
}
Foo.showName.call(); // 浏览器输出:hank  nodejs输出:undefined
//这里call方法里面的第一个参数为空,默认指向window / global
// 虽然showName方法定义在Foo对象里,但是使用call方法后,将showName方法里面的this指向了window/global。因此最后输出 hank / undefined。

再举个例子:

 function foo(a,b){
     this.a =a;
     this.b =b;
     this.change = function(x,y){
         this.a = x;
         this.b = y;
     }
 }
 var foodA = new foo('tomato','fish');
 var foodB = {a:'lemon',b:'apple'};

 foodA.change.call(foodB,'chili','pear');
console.log(foodB.a);   // chili                 
console.log(foodB.b);   // pear               

`FoodB`调用`foodA``change`方法,将`foodA`中的`this`绑定到了对象`foodB`上。

4.1 Funtion.prototype.bind()方法

var name = 'hank';                                                               
function Foo(name) {                                                             
    this.name = name;                                                            
    this.getName = function () {                                                 
          setTimeout(function () {                                               
              console.log(this.name);                                            
          },100);                                                                
    }                                                                            
}                                                                                
var  f = new Foo('HANK');                                                        
f.getName();  // 这里浏览器上输出hank   node上输出undefined                                 
//因为这里的 setTimeout()定时函数,相当于window.setTimeout(), 因此this指向window,则this.name则为hank 

如果才能达到输出HANK呢?

var name = 'hank';                                     
function Foo(name) {                                   
    this.name = name; 
    // _self =this // 也要可以用变量存储方式,利用变量调用对象自身的属性name                               
    this.getName = function () {                       
          setTimeout(function () {                     
              console.log(this.name);                  
          }.bind(this),100); 
          //这里使用bind方法,绑定setTimeout里面的匿名函数的this一直指向Foo对象

    };                                                 
}                                                      
var  f = new Foo('HANK');                              
f.getName();  // 输出HANK  

注意:匿名函数使用bind(this)方法后创建了新的函数,这个新函数不管什么地方执行,this都指向Foo,而非window,因此输出了HANK。

另外还有几个要注意的地方:

  • setTimeout/setInterval/匿名函数执行的时候,this默认指向window对象,除非你手动改变this指向。
  • setTimeout方法是挂在window对象下的。《JavaScript高级程序设计》第二版中,写到:“超时调用的代码都是在全局作用域中执行的,因此函数中this的值在非严格模式下指向window对象,在严格模式下是undefined”。在这里,我们只讨论非严格模式。

    var name = ‘hank’;
    function Foo(){

    this.name = 'HANK';
    this.getName = function(){ 
        console.log(this.name);
    }
    setTimeout(this.getName,100);
    

    }
    var f = new Foo(); // 输出 hank
    //this.getName方法即构造函数Foo()里面定义的方法。50ms后,执行this.getName方法, this.getName里面的this此时便指向了window对象

上面代码要解决问题,可以用采用变量来保存Foo对象的方式,然后利用变量来访问Foo对象自身的属性。

_self = this;
this.getName = function(){ 
    console.log(_self.name);
}

匿名函数:

var name="hank";
var person={
    name:"HANK",
    showName:function(){
        console.log(this.name);
    }
    getName:function(){
        (function(content){
             content.showName();
        })(this)
    }
}
person.getName();  // hank
//同样this还是指向了window / global

稍加修改:

var name="hank";
    var person={
        name:"HANK",
        showName:function(){
            console.log(this.name);
        }
        getName:function(){
                 var _self  = this; //变量存储Foo对象
            (function(self){
                  console.log(this===global) //true 这里的this指向window/global                
                  self.showName();
            })(_self) //注意,IIFE立即执行函数创建了临时作用域
        }
    }
    person.getName();  // HANK

5 Eval函数

该函数执行的时候,this绑定到当前作用域的对象上。

var name="hank";
    var foo={
        name:"HANK",
        showName:function(){
            eval("console.log(this.name)");
        }
    }

    foo.showName();  //输出 "HANK"

    var f=foo.showName;  // 执行f,this绑定到了当前作用域window/global上
    f();  //浏览器输出:hank  node输出:undefined

箭头函数(=>)

ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。这其实和ES6之前代码中的self=this机制一样。

箭头函数普通函数之间有一个重要的差别:箭头函数没有自己的this值,其this值是继承外域的this值。同时箭头函数的绑定也不能使用call, apply, bind等方法来改变this的指向,
事实上箭头函数并不绑定 this,arguments,super(ES6),抑或 new.target(ES6)。箭头函数不会绑定那些局部变量,所有涉及它们的引用,都会沿袭向上查找外层作用域链的方案来处理

我们来看看箭头函数的词法作用域:

function foo() {                   
  //返回一个箭头函数
  return () => { 
          //this 继承自foo()              
     console.log("id:", this.id); 
  };                         
}
var a = {id:2};
var b= {id:3};                               

var f = foo.call(a);
f.call(b); // id: 2

foo()内部创建的箭头函数会捕获调用时foo()this。由于foo()this绑定到a,f (引用箭头函数)的this的也会绑定到a。箭头函数的绑定无法被修改。(new 也不行!)

* lexical capture of this ?
在之前我也认为箭头函数里的this是局部的,啥意思呢?

箭头函数常用于回调函数中,例如事件处理器或者定时器:

function foo() {                   
  setTimeout( () => { 
          //这里的this在词法上继承自foo()            
     console.log("id:", this.id); 
  },100);                         
}           
foo.call( { id: 10 } ); // id: 10       

这里的箭头函数看起来把它内部的this绑定为父级函数foo()里的this了,如果这个内部函数式一个常规的函数(声明/表达式),它的this将类似setTimeout如何调用函数一个被控制着。
箭头函数可以像bind(…)一样确保函数的this被绑定到指定对象,其实就是它用更常见的词法作用域取代了传统的this机制。实际上,在ES6之前我们就已经在使用一种类似和箭头函数完全一样的模式。

function foo() {                               
 var context = this;  //  想要把this 指向函数自己,结果并没有  ,准确的说这里的this是一个动态上下文
 console.log(context); // {id:3}                          
  setTimeout(function() {                                              
     console.log("id:", context.id);           
  },100);                                      
}                                              
var a = {id:2};                                
var b= {id:3};
var f = foo.call(b);  // id: 3                           

虽然context=this和箭头函数看起来都可以取代bind(…)但是从本质上看,他们像替代的是this机制。

箭头函数没有自己的this,但当你在内部使用了this,常规的局部作用域准则就起作用了,它会指向最近一层作用域内的this.
看代码:

function foo(){
    return ()=>{
        return ()=>{
            return ()=>{
                console.log('id:',this.id);
            }
        }
    }
}
foo.call( { id: 12} )()()();   // 12

看上面代码,我有个问题,这里执行了多少次this绑定呢?
大部分人会认为有4次–每个函数里各一次
上面答案肯定是不对的, 准确的说,只有一次绑定,发生在foo()函数中。
这些链接内嵌的函数都没有声明他们自己的this,所以this.id的引用会简单的顺着作用域链查找,一直查到foo()函数,它是第一处能找到一个确切存在的this的地方。
其实this生来就是局部的,而且一直保持局部状态,箭头函数并不会绑定一个this变量,它的作用域会如同寻常所做的一样一层一层地向上查找。

除了this,还有argumentssuper(ES6) 、new.target(ES6) 匿名函数都是不绑定的

再来看一个例子:

function foo() {
   setTimeout( () => {
      console.log("args:", arguments);
   },100);
}

foo( 1,2,3,4 );
// args: [1,2,3,4]

代码中箭头函数并没有绑定 arguments,所以它会以 foo() 的 arguments 来取而代之,而 super 和 new.target 也是一样的情况。就到这里吧,欢迎各位拍砖。

文章目录
  1. 1. this指向哪里呢?
    1. 1.0.1. 1.普通函数调用
    2. 1.0.2. 2.作为方法来调用
    3. 1.0.3. 3.用作构造函数来调用
      1. 1.0.3.1. 3.1 new 操作符
    4. 1.0.4. 4.apply/call 方法的调用 ——————最大作用:就是改变this指向
    5. 1.0.5. 4.1 Funtion.prototype.bind()方法
    6. 1.0.6. 5 Eval函数
    7. 1.0.7. 箭头函数(=>)