深入理解js中的this

深入理解js中的this

学过javascript的同学想必都听过对this指向的描述,谁调用就指向谁,但究竟是怎么回事,欢迎往下看。

先来看看以下例子:

1
2
3
4
5
6
7
8
var obj = {
bar:1,
foo:function(){console.log(this.bar);}
};
var foo = obj.foo;
var bar = 2;
console.log(obj.foo()); //1
console.log(foo()); //2

解释:

obj.foo()摆明了是obj调用其属性对象foo,所以里面的this指向的就是obj这个对象。

对于foo()来说,等价于window.foo()。我们习惯性将全局变量window的属性直接以属性名展现。

JavaScript语言之所以有this的设计,跟内存里面的数据结果有关。

内存的数据结构

1
var obj = {foo : 5};

上面的代码将一个对象赋值给变量obj。js引擎会先在内存里面生成一个对象{foo:5},然后把这个对象的内存地址赋值给变量obj

也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。

原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的。

img

1
2
3
4
5
6
7
  foo: {
[[value]]: 5
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}

注意foo属性的值保存在属性描述对象value属性里。

若属性的值是一个函数呢
1
var obj = { foo: function () {} };

这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性。

由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。

1
2
3
4
5
6
7
8
var f = function () {console.log(this);};
var obj = { f: f };

// 单独执行,相当于Window.f()
f() //Window

// obj 环境执行
obj.f() //obj

js 允许在函数体内部,引用当前环境的其他变量。

this的设计目的就是在函数体内部,指代函数当前的运行环境。

img

this的实例讲解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var o = {
a:10,
b:{
a:12,
fn:function(){
console.log(this.a);
}
},
fn:function(){
console.log(this.a);
}
}
o.b.fn(); //12
o.fn(); //10

o.b.fn()因为函数fn()是被b调用,所以this指向b,this.a的值自然是b.a

由此我们可以总结出几种情况:

 情况1:如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window,这里需要说明的是在js的严格版中this指向的不是window,但是我们这里不探讨严格版的问题,你想了解可以自行上网查找。

 情况2:如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。

 情况3:如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象

1
2
3
4
5
6
7
8
9
10
var o = {
a:10,
b:{
// a:12,
fn:function(){
console.log(this.a);
}
}
}
o.b.fn(); //undefined

尽管对象b中没有属性a,这个this指向的也是对象b,因为this只会指向它的上一级对象,不管这个对象中有没有this要的东西。

还有一种比较特殊的情况

1
2
3
4
5
6
7
8
9
10
11
12
var o = {
a:10,
b:{
a:12,
fn:function(){
console.log(this.a); //undefined
console.log(this); //window
}
}
}
var j = o.b.fn;
j();

这里this指向的是window,是不是有些蒙了?其实是因为你没有理解一句话,这句话同样至关重要。

this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的,例子中虽然函数fn是被对象b所引用,但是在将fn赋值给变量j的时候并没有执行所以最终指向的是window。

构造函数版this

1
2
3
4
5
function Fn(){
this.user = "nikita";
}
var a = new Fn();
console.log(a.user); //nikita

  这里之所以对象a可以点出函数Fn里面的user是因为new关键字可以改变this的指向,将这个this指向对象a,为什么我说a是对象,因为用了new关键字就是创建一个对象实例,我们这里用变量a创建了一个Fn的实例(相当于复制了一份Fn到对象a里面),此时仅仅只是创建,并没有执行,而调用这个函数Fn的是对象a,那么this指向的自然是对象a,那么为什么对象a中会有user,因为你已经复制了一份Fn函数到对象a中,用了new关键字就等同于复制了一份。

当this遇上return时

先对比一下几个例子:

返回值是一个对象

1
2
3
4
5
6
7
function fn()  
{
this.user = 'nikita';
return {};
}
var a = new fn;
console.log(a.user); //undefined

返回一个函数

1
2
3
4
5
6
7
function fn()  
{
this.user = 'nikita';
return function(){};
}
var a = new fn;
console.log(a.user); //undefined

返回普通值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function fn()  
{
this.user = 'nikita';
return 1;
}
var a = new fn;
console.log(a.user); //nikita

function fn()
{
this.user = 'nikita';
return undefined;
}
var a = new fn;
console.log(a.user); //nikita

由此可知:

 如果返回值是一个对象/函数,那么this指向的就是那个返回的对象/函数,如果返回值不是一个对象/函数那么this还是指向函数的实例。

不过有一点需要注意就是:

虽然null也是对象,但this还是指向那个函数的实例

1
2
3
4
5
6
7
function fn()  
{
this.user = 'nikita';
return null;
}
var a = new fn;
console.log(a.user); //nikita

知识点补充:

  1.在严格版中的默认的this不再是window,而是undefined。

  2.new操作符会改变函数this的指向问题,虽然我们上面讲解过了,但是并没有深入的讨论这个问题,网上也很少说,所以在这里有必要说一下。

1
2
3
4
5
6
var num = 1;
function fn(){
this.num = 2;
}
var a = new fn();
console.log(a.num); //2

 为什么this会指向a?首先new关键字会创建一个空的对象,然后会自动调用将this指向这个空对象,这样的话函数内部的this就会被这个空的对象替代。

我们来看看new时发生的4个阶段。

1
var Func = function(){};
  1. 创建一个空对象
    1
    var obj = new Func();
  2. 设置原型链
    1
    obj._proto_ = Func.prototype;
  3. 让Func中的this指向obj,并执行Func的函数体。
    1
    var res = Func.call(obj);
  4. 判断Func的返回值类型

    如果是值类型,返回obj。如果是引用类型,就返回这个引用类型的对象。

    1
    2
    3
    4
    5
    6
    if (typeof(result) == "object"){
    func=result;
    }
    else{
    func=obj;;
    }

vue中的this及其作用域

这是vue文档里的原话:

All lifecycle hooks are called with their ‘this’ context pointing to the Vue instance invoking it.

意思是:在Vue所有的生命周期钩子方法(如created,mounted, updated以及destroyed)里使用this,this指向调用它的Vue实例,即(new Vue)。

箭头函数中的this

箭头函数没有自己的this和作用域,它的this是继承来的;默认指向在定义时他所处的对象(宿主环境),而不是执行时的对象,定义时,它的对象环境可能是window;箭头函数可以方便让我们在setTimeout,setInterval中方便的使用this。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var app = new Vue({
el : '#app',
data:{
message : '你好'
},
created : function(){
console.log(this.message); //你好 this指向vue实例
this.showMessage1(); //hello
this.showMessage2(); //你好
},
methods:{
showMessage1:function(){
setTimeout(function(){
document.getElementById('id1').innerText = this.message; //this指向window
},10)
},
showMessage2:function(){
setTimeout(() => {
document.getElementById('id2').innerText = this.message; //this指向vue实例
},10);
}
}
});

第一个输出“hello”,第二个输出“你好”。说明showMessage1()中的this指向的是window,showMessage()指向vue实例组件。

绑定vue实例到this的方法

为防止this指向出现歧义,有两种方法绑定this。

使用bind

1
2
3
4
5
showMessage:function(){
setTimeout(function(){
console.log(this.message);
}.bind(this),10);
}

对setTimeout()里的匿名函数使用bind()绑定到vue实例的this。这样在匿名函数中的this也是vue实例了。

把vue实例的this赋值给另一个变量再使用。

1
2
3
4
5
6
showMessage:function(){
var self = this;
setTimeout(function(){
console.log(self.message); //改为self
}.bind(this),10);
}

总结:

对于普通函数(包括匿名函数),this指向的是直接调用者,在非严格状态下,如果没有直接调用者,this指向window。在严格状态下,如果没有直接调用者,this为undefined。

箭头函数没有自己的作用域,在他内部使用的this是有它的宿主环境决定。

使用call,apply,bind绑定的,this指向绑定的对象。

this绑定的优先级

this绑定的优先级:

new > bind > call(apply) > obj.func() > 默认绑定。

1
2
3
4
var obj = {};
obj.log = console.log;
obj.log.call(console,this);
// 输出window

obj.log = console.log 为obj对象创建了一个函数(即console.log)的引用log,因为这是个函数的引用,所以想要执行有两种方法,一种是直接加上(),另一种如上转变this指向。而obj.log.call(console,this)中的第二个参数是从外部传进去的,和函数内的this对象无任何关系。

文章目录
  1. 1. 深入理解js中的this
    1. 1.0.1. 内存的数据结构
      1. 1.0.1.0.1. 若属性的值是一个函数呢
  2. 1.0.2. this的实例讲解
    1. 1.0.2.0.1. 当this遇上return时
    2. 1.0.2.0.2. 创建一个空对象
    3. 1.0.2.0.3. 设置原型链
    4. 1.0.2.0.4. 让Func中的this指向obj,并执行Func的函数体。
    5. 1.0.2.0.5. 判断Func的返回值类型
  • 2. vue中的this及其作用域
    1. 2.0.1. 箭头函数中的this
    2. 2.0.2. 绑定vue实例到this的方法
    3. 2.0.3. 总结:
  • 3. this绑定的优先级
  • |