一篇文章带你彻底理解this

走在前端的大道上javascript

本篇将本身读过的相关 this指向 的文章中,对本身有启发的章节片断总结在这(会对原文进行删改),会不断丰富提炼总结更新。css

版本一

一句话

this的指向在函数定义的时候是肯定不了的,只有函数执行的时候才能肯定this到底指向谁,实际上this的最终指向的是那个调用它的对象(这句话有些问题,后面会解释为何会有问题,虽然网上大部分的文章都是这样说的,虽然在不少状况下那样去理解不会出什么问题,可是实际上那样理解是不许确的,因此在你理解this的时候会有种琢磨不透的感受) —— —— 完全理解js中this的指向,没必要硬背。

5大规则

(1)构造函数模式的时候,this指向新生成的实例

function Aaa(name){
  this.name= name;
  this.getName=function(){
    console.log(this.name)
  }
}
var a = new Aaa('kitty');
a.getName()        //  'kitty'
var b = new Aaa('bobo');
b.getName()        //  'bobo'

若是 new 关键词出如今被调用函数的前面,那么JavaScript引擎会建立一个新的对象,被调用函数中的this指向的就是这个新建立的函数。html

function ConstructorExample() {
    console.log(this);
    this.value = 10;
    console.log(this);
}

new ConstructorExample();

// -> ConstructorExample {}
// -> ConstructorExample { value: 10 }

构造函数版this:前端

function Fn(){
    this.user = "追梦子";
}
var a = new Fn();
console.log(a.user); //追梦子

  这里之因此对象a能够点出函数Fn里面的user是由于new关键字能够改变this的指向,将这个this指向对象a,为何我说a是对象,由于用了new关键字就是建立一个对象实例,咱们这里用变量a建立了一个Fn的实例(至关于复制了一份Fn到对象a里面),此时仅仅只是建立,并无执行,而调用这个函数Fn的是对象a,那么this指向的天然是对象a,那么为何对象a中会有user,由于你已经复制了一份Fn函数到对象a中,用了new关键字就等同于复制了一份。vue

(2)apply/call调用模式的时候,this指向apply/call方法中的第一个参数

var list1 = {name:'andy'}
var list2 = {name:'peter'}

function d(){
  console.log(this.name)
}
d.call(list1)     //  'andy' 
d.call(list2)     //  'peter'

若是经过apply、call或者bind的方式触发函数,那么函数中的this指向传入函数的第一个参数。java

function fn() {
    console.log(this);
}

var obj = {
    value: 5
};

var boundFn = fn.bind(obj);

boundFn(); // -> { value: 5 }
fn.call(obj); // -> { value: 5 }
fn.apply(obj); // -> { value: 5 }

在没有学以前,一般会有这些问题。jquery

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this.user);
    }
}
var b = a.fn;
b(); //undefined

咱们是想打印对象a里面的user却打印出来undefined是怎么回事呢?若是咱们直接执行a.fn()是能够的。git

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this.user);
    }
}
a.fn(); //追梦子

虽然这种方法能够达到咱们的目的,可是有时候咱们不得不将这个对象保存到另外的一个变量中,那么就能够经过如下方法。github

一、call()

 

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this.user); //追梦子
    }
}
var b = a.fn;
b.call(a);

经过在call方法,给第一个参数添加要把b添加到哪一个环境中,简单来讲,this就会指向那个对象。web

call方法除了第一个参数之外还能够添加多个参数,以下:

var a = {
    user:"追梦子",
    fn:function(e,ee){
        console.log(this.user); //追梦子
        console.log(e+ee); //3
    }
}
var b = a.fn;
b.call(a,1,2);

二、apply()

apply方法和call方法有些类似,它也能够改变this的指向

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this.user); //追梦子
    }
}
var b = a.fn;
b.apply(a);

一样apply也能够有多个参数,可是不一样的是,第二个参数必须是一个数组,以下:

var a = {
    user:"追梦子",
    fn:function(e,ee){
        console.log(this.user); //追梦子
        console.log(e+ee); //11
    }
}
var b = a.fn;
b.apply(a,[10,1]);

或者

var a = {
    user:"追梦子",
    fn:function(e,ee){
        console.log(this.user); //追梦子
        console.log(e+ee); //520
    }
}
var b = a.fn;
var arr = [500,20];
b.apply(a,arr);

//注意若是call和apply的第一个参数写的是null,那么this指向的是window对象

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this); //Window {external: Object, chrome: Object, document: document, a: Object, speechSynthesis: SpeechSynthesis…}
    }
}
var b = a.fn;
b.apply(null);

三、bind()

bind方法和call、apply方法有些不一样,可是无论怎么说它们均可以用来改变this的指向。

先来讲说它们的不一样吧。

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this.user);
    }
}
var b = a.fn;
b.bind(a);

咱们发现代码没有被打印,对,这就是bind和call、apply方法的不一样,实际上bind方法返回的是一个修改事后的函数。

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this.user);
    }
}
var b = a.fn;
var c = b.bind(a);
console.log(c); //function() { [native code] }

那么咱们如今执行一下函数c看看,能不能打印出对象a里面的user

var a = {
    user:"追梦子",
    fn:function(){
        console.log(this.user); //追梦子
    }
}
var b = a.fn;
var c = b.bind(a);
c();

ok,一样bind也能够有多个参数,而且参数能够执行的时候再次添加,可是要注意的是,参数是按照形参的顺序进行的。

var a = {
    user:"追梦子",
    fn:function(e,d,f){
        console.log(this.user); //追梦子
        console.log(e,d,f); //10 1 2
    }
}
var b = a.fn;
var c = b.bind(a,10);
c(1,2);

总结:call和apply都是改变上下文中的this并当即执行这个函数,bind方法可让对应的函数想何时调就何时调用,而且能够将参数在执行的时候添加,这是它们的区别,根据本身的实际状况来选择使用。

(3)方法调用模式的时候,this指向方法所在的对象

var a={};
a.name = 'hello';
a.getName = function(){
  console.log(this.name)
}
a.getName()         //'hello'

若是一个函数是某个对象的方法,而且对象使用句点符号触发函数,那么this指向的就是该函数做为那个对象的属性的对象,也就是,this指向句点左边的对象。

var obj = {
    value: 5,
    printThis: function() {
      console.log(this);
    }
};

obj.printThis(); // -> { value: 5, printThis: ƒ }

由浅入深

例子1:

function a(){
    var user = "追梦子";
    console.log(this.user); //undefined
    console.log(this); //Window
}
a();

按照咱们上面说的this最终指向的是调用它的对象,这里的函数a实际是被Window对象所点出来的,下面的代码就能够证实。

function a(){
    var user = "追梦子";
    console.log(this.user); //undefined
    console.log(this);  //Window
}
window.a();

和上面代码同样吧,其实alert也是window的一个属性,也是window点出来的。

例子2:

var o = {
    user:"追梦子",
    fn:function(){
        console.log(this.user);  //追梦子
    }
}
o.fn();

  这里的this指向的是对象o,由于你调用这个fn是经过o.fn()执行的,那天然指向就是对象o,这里再次强调一点,this的指向在函数建立的时候是决定不了的,在调用的时候才能决定,谁调用的就指向谁,必定要搞清楚这个。

其实例子1和例子2说的并不够准确,下面这个例子就能够推翻上面的理论。

若是要完全的搞懂this必须看接下来的几个例子

例子3:

var o = {
    user:"追梦子",
    fn:function(){
        console.log(this.user); //追梦子
    }
}
window.o.fn();

  这段代码和上面的那段代码几乎是同样的,可是这里的this为何不是指向window,若是按照上面的理论,最终this指向的是调用它的对象,这里先说个而外话,window是js中的全局对象,咱们建立的变量其实是给window添加属性,因此这里能够用window点o对象。

  这里先不解释为何上面的那段代码this为何没有指向window,咱们再来看一段代码。

var o = {
    a:10,
    b:{
        a:12,
        fn:function(){
            console.log(this.a); //12
        }
    }
}
o.b.fn();

  这里一样也是对象o点出来的,可是一样this并无执行它,那你确定会说我一开始说的那些不就都是错误的吗?其实也不是,只是一开始说的不许确,接下来我将补充一句话,我相信你就能够完全的理解this的指向的问题。

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

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

  - 状况3:若是一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象,例子3能够证实,若是不相信,那么接下来咱们继续看几个例子。

var o = {
    a:10,
    b:{
        // a:12,
        fn:function(){
            console.log(this.a); //undefined
        }
    }
}
o.b.fn();

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

还有一种比较特殊的状况,例子4:

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永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的,例子4中虽然函数fn是被对象b所引用,可是在将fn赋值给变量j的时候并无执行因此最终指向的是window,这和例子3是不同的,例子3是直接执行了fn。

  this讲来说去其实就是那么一回事,只不过在不一样的状况下指向的会有些不一样,上面的总结每一个地方都有些小错误,也不能说是错误,而是在不一样环境下状况就会有不一样,因此我也没有办法一次解释清楚,只能你慢慢地的去体会。

(4)函数调用模式的时候,this指向window

function aa(){
  console.log(this)
}
aa()         //window

若是一个函数做为FFI被调用,意味着这个函数不符合以上任意一种调用方式,this指向全局对象,在浏览器中,便是window。

function fn() {
    console.log(this);
}

// If called in browser:
fn(); // -> Window {stop: ƒ, open: ƒ, alert: ƒ, ...}

注意,第4条规则和第3条很相似,不一样的是当函数没有做为方法被调用时,它将自动隐式编程全局对象的属性——window。也就是当咱们调用 fn(),能够理解为window.fn(),根据第三条规则,fn()函数中的this指向的就是window。

function fn() {
    console.log(this);
}

// In browser:
console.log(fn === window.fn); // -> true

(5) 若是出现上面对条规则的累加状况,则优先级自1至4递减,this的指向按照优先级最高的规则判断。

将规则应用于实践
看一个代码示例,并使用上面的规则判断this的指向。

var obj = {
    value: 'hi',
    printThis: function() {
        console.log(this);
    }
};

var print = obj.printThis;

obj.printThis(); // -> {value: "hi", printThis: ƒ}
print(); // -> Window {stop: ƒ, open: ƒ, alert: ƒ, ...}

obj.prinThis() ,根据第三条规则this指向的就是obj。根据第四条规则print()是FFI,所以this指向window。

obj对象中printThis这一方法实际上是函数的地址的一个引用,当咱们将obj.printThis赋值给print时,print包含的也是函数的引用,和obj对象一点关系也没有。obj只是碰巧拥有一个指向这个函数的引用的属性。

当不适用obj对象触发函数时,这个函数就是FFI。

应用多项规则
当出现多个上述规则时,将优先级高的“获胜”,若是规则2和规则3同时存在,则规则2优先:

var obj1 = {
    value: 'hi',
    print: function() {
        console.log(this);
    },
};

var obj2 = { value: 17 };

obj1.print.call(obj2); // -> { value: 17 }

若是规则1和规则3同时被应用,则规则1优先:

var obj1 = {
    value: 'hi',
    print: function() {
        console.log(this);
    },
};

new obj1.print(); // -> print {}

额外的

当this碰到return时

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

再看一个

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

再来

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

什么意思呢?

  若是返回值是一个对象,那么this指向的就是那个返回的对象,若是返回值不是一个对象那么this仍是指向函数的实例。

function fn()  
{  
    this.user = '追梦子';  
    return undefined;
}
var a = new fn;  
console.log(a); //fn {user: "追梦子"}

  还有一点就是虽然null也是对象,可是在这里this仍是指向那个函数的实例,由于null比较特殊。

function fn()  
{  
    this.user = '追梦子';  
    return null;
}
var a = new fn;  
console.log(a.user); //追梦子

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

代码中引用了库?

有些库会将this的指向绑定更有用的对象上,好比jQuery库,在事件处理程序中,this的指向不是全局对象而被绑定到了元素对象上。所以,若是你发现一些不能用上述5项规则解释的状况,请阅读你所使用的库的官方文档,找到关于该库是如何改变this的指向的,一般经过 bind 方法改变this的指向。

参考文章:
1.完全理解js中this的指向,没必要硬背。
2.javascript中this指向的规则
3.The Complete Rules to 'this'
4.JavaScript中的this


版本二

上下文 vs 做用域

每一个函数调用都有与之相关的做用域和上下文。首先须要澄清的问题是上下文和做用域是不一样的概念。不少人常常将这两个术语混淆。

做用域(scope) 是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,做用域决定了代码区块中变量和其余资源的可见性。而上下文(context)是用来指定代码某些特定部分中 this 的值。

从根本上说,做用域是基于函数(function-based)的,而上下文是基于对象(object-based)的。换句话说,做用域是和每次函数调用时变量的访问有关,而且每次调用都是独立的。上下文老是被调用函数中关键字 this 的值,是调用当前可执行代码的对象的引用。说的通俗一点就是:this 取值,是在函数真正被调用执行的时候肯定的,而不是在函数定义的时候肯定的。

全局上下文

不管是否在严格模式下,在全局执行上下文中(在任何函数体外部)this 都指向全局对象。固然具体的全局对象和宿主环境有关。

在浏览器中, window 对象同时也是全局对象:

console.log(this === window); // true

NodeJS 中,则是 global 对象:

console.log(this); // global

函数上下文

因为其运行期绑定的特性,JavaScript 中的 this 含义要丰富得多,它能够是全局对象、当前对象或者任意对象,这彻底取决于函数的调用方式。JavaScript 中函数的调用有如下几种方式:做为函数调用,做为对象方法调用,做为构造函数调用,和使用 apply 或 call 调用。下面咱们将按照调用方式的不一样,分别讨论 this 的含义

做为函数直接调用

做为函数直接调用时,要注意 2 种状况:

非严格模式

在非严格模式下执行函数调用,此时 this 默认指向全局对象。

function f1(){
  return this;
}
//在浏览器中:
f1() === window;   //在浏览器中,全局对象是window
 
//在Node中:
f1() === global;

严格模式 ‘use strict’

在严格模式下,this 将保持他进入执行上下文时的值,因此下面的 this 并不会指向全局对象,而是默认为 undefined 。

'use strict'; // 这里是严格模式
function test() {
  return this;
};
 
test() === undefined; // true

做为对象的方法调用

在 JavaScript 中,函数也是对象,所以函数能够做为一个对象的属性,此时该函数被称为该对象的方法,在使用这种调用方式时,内部的 this 指向该对象。

var Obj = {
  prop: 37,
  getProp: function() {
    return this.prop;
  }
};
 
console.log(Obj.getProp()); // 37

上面的例子中,当 Obj.getProp() 被调用时,方法内的 this 将指向 Obj 对象。值得注意的是,这种行为根本不受函数定义方式或定义位置的影响。在前面的例子中,咱们在定义对象 Obj 的同时,将成员 getProp 定义了一个匿名函数。可是,咱们也能够首先定义函数,而后再将其附加到 Obj.getProp 。因此,下面的代码和上面的例子是等价的:

var Obj = {
  prop: 37
};
 
function independent() {
  return this.prop;
}
 
Obj.getProp = independent;
 
console.log(Obj.getProp()); // logs 37

JavaScript 很是灵活,如今咱们把对象的方法赋值给一个变量,而后直接调用这个函数变量又会发生什么呢?

var Obj = {
  prop: 37,
  getProp: function() {
    return this.prop;
  }
};
 
var test = Obj.getProp
console.log(test()); // undefined

能够看到,这时候 this 指向全局对象,这个例子 test 只是引用了 Obj.getProp 函数,也就是说这个函数并不做为 Obj 对象的方法调用,因此,它是被看成一个普通函数来直接调用。所以,this 指向全局对象。

一些坑

咱们来看看下面这个例子:

var prop = 0;
var Obj = {
  prop: 37,
  getProp: function() {
    setTimeout(function() {
        console.log(this.prop) // 结果是 0 ,不是37!
    },1000)
  }
};
 
Obj.getProp();

正如你所见, setTimeout 中的 this 向了全局对象,这里不是把它看成函数的方法使用吗?这一点常常让不少初学者疑惑;这种问题是不少异步回调函数中也会广泛会碰到,一般有个土办法解决这个问题,好比,咱们能够利用 闭包 的特性来处理:

var Obj = {
  prop: 37,
  getProp: function() {
    var self = this; 
    setTimeout(function() {
        console.log(self.prop) // 37
    },1000)
  }
};
 
Obj.getProp();

其实,setTimeoutsetInterval 都只是在全局上下文中执行一个函数而已,即便是在严格模式下:

'use strict';
 
function foo() {
  console.log(this); // Window
}
 
setTimeout(foo, 1);

记住 setTimeoutsetInterval 都只是在全局上下文中执行一个函数而已,所以 this 指向全局对象。 除非你实用箭头函数,Function.prototype.bind 方法等办法修复。至于解决方案会在后续的文章中继续讨论。

做为构造函数调用

JavaScript 支持面向对象式编程,与主流的面向对象式编程语言不一样,JavaScript 并无类(class)的概念,而是使用基于原型(prototype)的继承方式。做为又一项约定通用的准则,构造函数以大写字母开头,提醒调用者使用正确的方式调用。

当一个函数用做构造函数时(使用 new 关键字),它的 this 被绑定到正在构造的新对象,也就是咱们常说的实例化出来的对象。

function Person(name) {
  this.name = name;
}
 
var p = new Person('愚人码头');
console.log(p.name); // "愚人码头"

几个陷阱

若是构造函数具备返回对象的 return 语句,则该返回对象将是 new 表达式的结果。

function Person(name) {
  this.name = name;
  return { title : "前端开发" };
}
 
var p = new Person('愚人码头');
console.log(p.name); // undefined
console.log(p.title); // "前端开发"

相应的,JavaScript 中的构造函数也很特殊,若是不使用 new 调用,则和普通函数同样, this 仍然执行全局:

function Person(name) {
  this.name = name;
  console.log(this); // Window 
}
 
var p = Person('愚人码头');

箭头函数中的 this

在箭头函数中,this 与封闭词法上下文的 this 保持一致,也就是说由上下文肯定。

var obj = {
    x: 10,
    foo: function() {
        var fn = () => {
            return () => {
                return () => {
                    console.log(this);      //{x: 10, foo: ƒ} 即 obj
                    console.log(this.x);    //10
                }
            }
        }
        fn()()();
    }
}
obj.foo();

obj.foo 是一个匿名函数,不管如何, 这个函数中的 this 指向它被建立时的上下文(在上面的例子中,就是 obj 对象)。这一样适用于在其余函数中建立的箭头函数:这些箭头函数的this 被设置为外层执行上下文

// 建立一个含有bar方法的obj对象,bar返回一个函数,这个函数返回它本身的this,
// 这个返回的函数是以箭头函数建立的,因此它的this被永久绑定到了它外层函数的this。
// bar的值能够在调用中设置,它反过来又设置返回函数的值。
var obj = {
    bar: function() {
        var x = (() => this);
        return x;
    }
};
 
// 做为obj对象的一个方法来调用bar,把它的this绑定到obj。
// x所指向的匿名函数赋值给fn。
var fn = obj.bar();
 
// 直接调用fn而不设置this,一般(即不使用箭头函数的状况)默认为全局对象,若在严格模式则为undefined
console.log(fn() === obj); // true
 
// 可是注意,若是你只是引用obj的方法,而没有调用它(this是在函数调用过程当中设置的)
var fn2 = obj.bar;
// 那么调用箭头函数后,this指向window,由于它从 bar 继承了this。
console.log(fn2()() == window); // true

在上面的例子中,一个赋值给了 obj.bar 的函数(称为匿名函数 A),返回了另外一个箭头函数(称为匿名函数 B)。所以,函数B的this被永久设置为 obj.bar(函数A)被调用时的 this 。当返回的函数(函数B)被调用时,它this始终是最初设置的。在上面的代码示例中,函数B的 this 被设置为函数A的 this ,即 obj,因此它仍然设置为 obj,即便以一般将 this 设置为 undefined 或全局对象(或者如前面示例中全局执行上下文中的任何其余方法)进行调用。

填坑

咱们回到上面 setTimeout 的坑:

var prop = 0;
var Obj = {
  prop: 37,
  getProp: function() {
    setTimeout(function() {
        console.log(this.prop) // 结果是 0 ,不是37!
    },1000)
  }
};
 
Obj.getProp();

一般状况我,咱们在这里指望输出的结果是 37 ,用箭头函数解决这个问题至关简单:

var Obj = {
  prop: 37,
  getProp: function() {
    setTimeout(() => {
        console.log(this.prop) // 37
    },1000)
  }
};
 
Obj.getProp();

原型链中的 this

相同的概念在定义在原型链中的方法也是一致的。若是该方法存在于一个对象的原型链上,那么 this 指向的是调用这个方法的对象,就好像该方法原本就存在于这个对象上。

var o = {
  f : function(){ 
    return this.a + this.b; 
  }
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
 
console.log(p.f()); // 5

在这个例子中,对象 p 没有属于它本身的f属性,它的f属性继承自它的原型。可是这对于最终在 o 中找到 f 属性的查找过程来讲没有关系;查找过程首先从 p.f 的引用开始,因此函数中的 this 指向 p 。也就是说,由于f是做为p的方法调用的,因此它的this 指向了 p 。这是 JavaScript 的原型继承中的一个有趣的特性。

你也会看到下面这种形式的老代码,道理是同样的:

function Person(name) {
  this.name = name;
}
Person.prototype = {
  getName:function () {
    return this.name
  }
};
var p = new Person('愚人码头');
console.log(p.getName()); // "愚人码头"

getter 与 setter 中的 this

再次,相同的概念也适用时的函数做为一个 getter 或者 一个 setter 调用。用做 getter 或 setter 的函数都会把 this 绑定到正在设置或获取属性的对象。

function sum() {
  return this.a + this.b + this.c;
}
 
var o = {
  a: 1,
  b: 2,
  c: 3,
  get average() {
    return (this.a + this.b + this.c) / 3;
  }
};
 
Object.defineProperty(o, 'sum', {
    get: sum, enumerable: true, configurable: true});
 
console.log(o.average, o.sum); // logs 2, 6

注:Object.defineProperty() 顾名思义,为对象定义属性,方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。是ES5的属性, 支持IE8以上。

Object.defineProperty(obj, prop, descriptor)

参数

  • object 必需。 要在其上添加或修改属性的对象。 这多是一个本机 JavaScript对象(即用户定义的对象或内置对象)或 DOM 对象。
  • propertyname 必需。 一个包含属性名称的字符串。
  • descriptor 必需。 属性描述符。 它能够针对数据属性或访问器属性。

在js中咱们能够经过下面这几种方法定义属性

// (1) define someOne property name
someOne.name = 'cover';
//or use (2) 
someOne['name'] = 'cover';
// or use (3) defineProperty
Object.defineProperty(someOne, 'name', {
    value : 'cover'
})

属性的状态设置
其中descriptor的参数值得咱们关注下,该属性可设置的值有:
【value】 属性的值,默认为 undefined。
【writable】 该属性是否可写,若是设置成 false,则任何对该属性改写的操做都无效(但不会报错),对于像前面例子中直接在对象上定义的属性,这个属性该特性默认值为为 true。

var someOne = { };
Object.defineProperty(someOne, "name", {
    value:"coverguo" , //因为设定了writable属性为false 致使这个量不能够修改
    writable: false 
});  
console.log(someOne.name); // 输出 coverguo
someOne.name = "linkzhu";
console.log(someOne.name); // 输出coverguo

【configurable]】若是为false,则任未尝试删除目标属性或修改属性如下特性(writable, configurable, enumerable)的行为将被无效化,对于像前面例子中直接在对象上定义的属性,这个属性该特性默认值为为 true。

var someOne = { };
Object.defineProperty(someOne, "name", {
    value:"coverguo" ,
    configurable: false 
});  
delete someOne.name; 
console.log(someOne.name);// 输出 coverguo
someOne.name = "linkzhu";
console.log(someOne.name); // 输出coverguo

【enumerable】 是否能在for-in循环中遍历出来或在Object.keys中列举出来。对于像前面例子中直接在对象上定义的属性,这个属性该特性默认值为为 true。
注意 在调用Object.defineProperty()方法时,若是不指定, configurable, enumerable, writable特性的默认值都是false,这跟以前所 说的对于像前面例子中直接在对象上定义的属性,这个特性默认值为为 true并不冲突,以下代码所示:

//调用Object.defineProperty()方法时,若是不指定
var someOne = { };
someOne.name = 'coverguo';
console.log(Object.getOwnPropertyDescriptor(someOne, 'name'));
//输出 Object {value: "coverguo", writable: true, enumerable: true, configurable: true}

//直接在对象上定义的属性,这个特性默认值为为 true
var otherOne = {};
Object.defineProperty(otherOne, "name", {
    value:"coverguo" 
});  
console.log(Object.getOwnPropertyDescriptor(otherOne, 'name'));
//输出 Object {value: "coverguo", writable: false, enumerable: false, configurable: false}

【get】一旦目标对象访问该属性,就会调用这个方法,并返回结果。默认为 undefined。
【set】 一旦目标对象设置该属性,就会调用这个方法。默认为 undefined。
从上面,能够得知,咱们能够经过使用Object.defineProperty,来定义和控制一些特殊的属性,如属性是否可读,属性是否可枚举,甚至修改属性的修改器(setter)和获取器(getter)
那什么场景和地方适合使用到特殊的属性呢?

从上面,能够得知,咱们能够经过使用Object.defineProperty,来定义和控制一些特殊的属性,如属性是否可读,属性是否可枚举,甚至修改属性的修改器(setter)和获取器(getter)

实际运用
在一些框架,如vue、express、qjs等,常常会看到对Object.defineProperty的使用。那这些框架是如何使用呢?

MVVM中数据‘双向绑定’实现
待补充
优化对象获取和修改属性方式
这个优化对象获取和修改属性方式,是什么意思呢? 过去咱们在设置dom节点transform时是这样的。

//加入有一个目标节点, 咱们想设置其位移时是这样的
var targetDom = document.getElementById('target');
var transformText = 'translateX(' + 10 + 'px)';
targetDom.style.webkitTransform = transformText;
targetDom.style.transform = transformText;

经过上面,能够看到若是页面是须要许多动画时,咱们这样编写transform属性是十分蛋疼的。
但若是经过Object.defineProperty, 咱们则能够

//这里只是简单设置下translateX的属性,其余如scale等属性可本身去尝试

Object.defineProperty(dom, 'translateX', {
set: function(value) {
         var transformText = 'translateX(' + value + 'px)';
        dom.style.webkitTransform = transformText;
        dom.style.transform = transformText;
}
//这样再后面调用的时候, 十分简单
dom.translateX = 10;
dom.translateX = -10;
//甚至能够拓展设置如scale, originX, translateZ,等各个属性,达到下面的效果
dom.scale = 1.5;  //放大1.5倍
dom.originX = 5;  //设置中心点X
}

上面只是个简单的版本,并非最合理的写法,但主要是为了说明具体的意图和方法
增长属性获取和修改时的信息
如在Express4.0中,该版本去除了一些旧版本的中间件,为了让用户可以更好地发现,其有下面这段代码,经过修改get属性方法,让用户调用废弃属性时抛错并带上自定义的错误信息。

[
  'json',
  'urlencoded',
  'bodyParser',
  'compress',
  'cookieSession',
  'session',
  'logger',
  'cookieParser',
  'favicon',
  'responseTime',
  'errorHandler',
  'timeout',
  'methodOverride',
  'vhost',
  'csrf',
  'directory',
  'limit',
  'multipart',
  'staticCache',
].forEach(function (name) {
  Object.defineProperty(exports, name, {
    get: function () {
      throw new Error('Most middleware (like ' + name + ') is no longer bundled with Express and must be installed separately. Please see https://github.com/senchalabs/connect#middleware.');
    },
    configurable: true
  });
});

做为一个DOM事件处理函数

当函数被用做事件处理函数时,它的 this 指向触发事件的元素(一些浏览器在使用非addEventListener 的函数动态添加监听函数时不遵照这个约定)。

// 被调用时,将关联的元素变成蓝色
function bluify(e){
  console.log(this === e.currentTarget); // 老是 true
 
  // 当 currentTarget 和 target 是同一个对象是为 true
  console.log(this === e.target);        
  this.style.backgroundColor = '#A5D9F3';
}
 
// 获取文档中的全部元素的列表
var elements = document.getElementsByTagName('*');
 
// 将bluify做为元素的点击监听函数,当元素被点击时,就会变成蓝色
for(var i=0 ; i < elements.length; i++){
  elements[i].addEventListener('click', bluify, false);
}

做为一个内联事件处理函数

当代码被内联on-event 处理函数调用时,它的this指向监听器所在的DOM元素:

<button onclick="alert(this.tagName.toLowerCase());">
  Show this
</button>

上面的 alert 会显示 button 。注意只有外层代码中的 this 是这样设置的:

<button onclick="alert((function(){return this})());">
  Show inner this
</button>

在这种状况下,没有设置内部函数的 this,因此它指向 global/window 对象(即非严格模式下调用的函数未设置 this 时指向的默认对象)。

使用 apply 或 call 调用

JavaScript 中函数也是对象,对象则有方法,apply 和 call 就是函数对象的方法。这两个方法异常强大,他们容许切换函数执行的上下文环境(context),即 this 绑定的对象。不少 JavaScript 中的技巧以及类库都用到了该方法。让咱们看一个具体的例子:

function Point(x, y){ 
   this.x = x; 
   this.y = y; 
   this.moveTo = function(x, y){ 
       this.x = x; 
       this.y = y; 
   } 
} 
 
var p1 = new Point(0, 0); 
p1.moveTo(1, 1); 
console.log(p1.x,p1.y); //1 1
 
var p2 = {x: 0, y: 0}; 
p1.moveTo.apply(p2, [10, 10]);
console.log(p2.x,p2.y); //10 10

在上面的例子中,咱们使用构造函数生成了一个对象 p1,该对象同时具备 moveTo 方法;使用对象字面量建立了另外一个对象 p2,咱们看到使用 apply 能够将 p1 的方法 apply 到 p2 上,这时候 this 也被绑定到对象 p2 上。另外一个方法 call 也具有一样功能,不一样的是最后的参数不是做为一个数组统一传入,而是分开传入的:

function Point(x, y){ 
   this.x = x; 
   this.y = y; 
   this.moveTo = function(x, y){ 
       this.x = x; 
       this.y = y; 
   } 
} 
 
var p1 = new Point(0, 0); 
p1.moveTo(1, 1); 
console.log(p1.x,p1.y); //1 1
 
var p2 = {x: 0, y: 0}; 
p1.moveTo.call(p2, 10, 10); // 只是参数不一样
console.log(p2.x,p2.y); //10 10

.bind() 方法

ECMAScript 5 引入了 Function.prototype.bind 。调用 f.bind(someObject) 会建立一个与 f 具备相同函数体和做用域的函数,可是在这个新函数中,this 将永久地被绑定到了 bind 的第一个参数,不管这个函数是如何被调用的。

function f(){
  return this.a;
}
 
//this被固定到了传入的对象上
var g = f.bind({a:"azerty"});
console.log(g()); // azerty
 
var h = g.bind({a:'yoo'}); //bind只生效一次!
console.log(h()); // azerty
 
var o = {a:37, f:f, g:g, h:h};
console.log(o.f(), o.g(), o.h()); // 37, azerty, azerty

填坑

上面咱们已经讲了使用箭头函数填 setTimeout 的坑,此次咱们使用 bind 方法来试试:

var prop = 0;
var Obj = {
  prop: 37,
  getProp: function() {
    setTimeout(function() {
        console.log(this.prop) // 37
    }.bind(Obj),1000)
  }
};
 
Obj.getProp();

一样能够填坑,可是看上去没有使用箭头函数来的优雅。

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实例。

示例分析

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.js"></script>
    <script src="https://unpkg.com/vue@2.5.9/dist/vue.js"></script>
</head>
<div id="app" style="width: 100%;height: auto;font-size:20px;">
    <p id="id1"></p>
    <p id="id2"></p>
</div>
<script type="text/javascript">
    var message = "Hello!";
    var app = new Vue({
        el:"#app",
        data:{
            message: "你好!"
        },
        created: function() {
          this.showMessage1();    //this 1
          this.showMessage2();   //this 2
        },
        methods:{
            showMessage1:function(){
                setTimeout(function() {
                   document.getElementById("id1").innerText = this.message;  //this 3
                }, 10)
            },
            showMessage2:function() {
                setTimeout(() => {
                   document.getElementById("id2").innerText = this.message;  //this 4
                }, 10)
            }
        }
    });
</script>
</html>

示例定义了两个message。一个是全局变量,即window.message,它的值为英文“Hello!”。另一个是vue实例的数据message,它的值为中文的“你好!”。

运行示例,在浏览器获得:
clipboard.png

第一个输出英文"Hello!”,第二个输出中文“你好!”。这说明了showMessage1()里的this指的是window,而showMessage2()里的this指的是vue实例。

//created

created: function() {
  this.showMessage1();    //this 1
  this.showMessage2();   //this 2
}

created函数为vue实例的钩子方法,它里面使用的this指的是vue实例。

//showMessage1()

showMessage1:function(){
    setTimeout(function() {
       document.getElementById("id1").innerText = this.message;  //this 3
    }, 10)
}

对于普通函数(包括匿名函数),this指的是直接的调用者,在非严格模式下,若是没有直接调用者,this指的是window。showMessage1()里setTimeout使用了匿名函数,this指向window。

//showMessage2()

showMessage2:function() {
    setTimeout(() => {
       document.getElementById("id2").innerText = this.message;  //this 4
    }, 10)
}

箭头函数是没有本身的this,在它内部使用的this是由它定义的宿主对象决定。showMessage2()里定义的箭头函数宿主对象为vue实例,因此它里面使用的this指向vue实例。

绑定vue实例到this的方法

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

使用bind

showMessage1()能够改成:

showMessage1:function(){
    setTimeout(function() {
       document.getElementById("id1").innerText = this.message;  //this 3
    }.bind(this), 10)
}

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

赋值给另外一个变量

showMessage1()也能够改成

showMessage1:function(){
    var self = this;
    setTimeout(function() {
       document.getElementById("id1").innerText = self.message;  //改成self
    }.bind(this), 10)
}

这里吧表示vue实例的this赋值给变量self。在使用到this的地方改用self引用。

参考文章:
1.全面理解 JavaScript 中的 this
2.不会Object.defineProperty你就out了
3.10道典型的JavaScript面试题

相关文章
相关标签/搜索