一篇文章帶你徹底理解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面試題

相關文章
相關標籤/搜索