《你不知道的js(上卷)》筆記2(this和對象原型)

書籍封面

學了多種語言,發現javascriptthis是最難以捉摸的。this不就是指向當前對象的指針嗎?可是結合上下文來看,卻又往往不知道this到底指的是誰了,所以Javascript最主要的兩個知識點,除了閉包,就是this了。

1. 關于this

this關鍵字是javascript中最復雜的機制之一。它是一個很特別的關鍵字,被自動定義在 所有函數的作用域中。

this提供了一種更優雅的方式來隱式“傳遞”一個對象引用,因此可以將API設計
得更加簡潔并且易于復用。

function identify() {
  return this.name.toUpperCase();
}

var me = {
  name: "Kyle"
};

identify.call( me ); // KYLE

this并不像我們所想的那樣指向函數本身。

function foo(num) {
  this.count++;
}

foo.count = 0;
var i;
for (i=0; i<10; i++) { 
      if (i > 5) {
        foo( i ); 
      }
}
console.log( foo.count ); // 0 

函數內部代碼this.count中的this并不是指向那個函數對象,所以雖然屬性名相同,根對象卻并不相同,困惑隨之產生。

函數內部代碼this.count最終值為NaN,同時也是全局變量。

可以使用函數名稱標識符來代替this來引用函數對象。這樣,更像是靜態變量。

function foo(num) {
  foo.count++;
}

foo.count = 0;
var i;
for (i=0; i<10; i++) { 
      if (i > 5) {
        foo( i ); 
      }
}
console.log( foo.count ); // 4

另外一種方式是強制this指向foo函數對象。

function foo(num) {
  this.count++;
}

foo.count = 0;
var i;
for (i=0; i<10; i++) { 
      if (i > 5) {
        foo.call(foo, i ); 
      }
}
console.log( foo.count ); // 4

this到底是什么

this是在運行時進行綁定的,并不是在編寫時綁定,它的上下文取決于函數調 用時的各種條件。this的綁定和函數聲明的位置沒有任何關系,只取決于函數的調用方式。

調用位置

函數被調用的位置。每個函數的 this 是在調用 時被綁定的,完全取決于函數的調用位置,因為它決定了this的綁定。

function baz() {
// 當前調用棧是:baz
// 因此,當前調用位置是全局作用域
   console.log( "baz" );
    bar(); // <-- bar 的調用位置 
}

function bar() {
    // 當前調用棧是 baz -> bar
    // 因此,當前調用位置在 baz 中
    console.log( "bar" );
    foo(); // <-- foo 的調用位置 
}

function foo() {
        // 當前調用棧是 baz -> bar -> foo 
        // 因此,當前調用位置在 bar 中
         console.log( "foo" );
}
baz(); // <-- baz 的調用位置

1.1 綁定規則

默認綁定

聲明在全局作用域中的變量就是全局對象的一個同名屬性。

function foo() { 
  console.log( this.a );
}

var a = 2; 
foo(); // 2

在本 例中,函數調用時應用了this的默認綁定,因此this指向全局對象。

foo()是直接使用不帶任何修飾的函數引用進行調用的,因此只能使用默認綁定,無法應用其他規則。

如果使用嚴格模式,那么全局對象將無法使用默認綁定,因此this會綁定到 undefined。

function foo() {
   "use strict";
    console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined

隱式綁定

如果調用位置是有上下文對象,或者被某個對象擁有或者包含,那么就可能隱式綁定。

function foo() { 
  console.log( this.a );
}
var obj = { 
  a: 2,
  foo: foo
};

var obj1 = { 
  a: 42,
  obj: obj
};

obj.foo(); // 2
obj1.obj.foo(); // 2

當函數引用有上下文對象時,隱式綁定規則會把函數調用中的this綁定到這個上下文對象。因為調 用foo()this被綁定到obj,因此this.aobj.a是一樣的。

對象屬性引用鏈中只有最頂層或者說最后一層會影響調用位置。

隱式綁定的函數可能會丟失綁定對象,而應用默認綁定,把this綁定到全局對象或者undefined上,取決于是否是嚴格模式。

function foo() { 
  console.log( this.a );
}
var obj = { 
  a: 2,
  foo: foo 
};
var bar = obj.foo; // 函數別名!
var a = "oops, global"; // a 是全局對象的屬性 
bar(); // "oops, global"

function doFoo(fn) {
    // fn 其實引用的是 foo 
  fn(); // <-- 調用位置!
}

doFoo( obj.foo ); // "oops, global"

barobj.foo的一個引用,bar()其實是一個不帶任何修飾的函數調用。

參數傳遞其實就是一種隱式賦值,因此我們傳入函數時也會被隱式賦值,所以結果一樣。

顯式綁定

可以使用函數的call(..)apply(..)方法實現顯式綁定。

function foo() { 
  console.log( this.a );
}
var obj = {
   a:2
};
foo.call( obj ); // 2

如下例子,無論bar綁定到哪個對象上,foo始終綁定在obj上,稱之為硬綁定。

function foo() { 
  console.log( this.a );
}
var obj = { 
  a:2
};
var bar = function() { 
  foo.call( obj );
};

bar.call( window ); // 2

在 ES5 中提供了內置的方法Function.prototype.bind就是硬綁定。

如果你把null或者undefined作為this的綁定對象傳入callapply或者 bind,這些值在調用時會被忽略,實際應用的是默認綁定規則。

new綁定

JavaScriptnew的機制實 際上和面向類的語言完全不同。

JavaScript中,構造函數只是一些 使用new操作符時被調用的函數。它們并不會屬于某個類,也不會實例化一個類。實際上,它們甚至都不能說是一種特殊的函數類型,它們只是被 new 操作符調用的普通函數而已。

使用new來調用函數,或者說發生構造函數調用時,會自動執行下面的操作。

  1. 創建(或者說構造)一個全新的對象。

  2. 這個新對象會被執行[[原型]]連接。

  3. 這個新對象會綁定到函數調用的this。

  4. 如果函數沒有返回其他對象,那么new表達式中的函數調用會自動返回這個新對象。

function foo(p1,p2) { 
  this.val = p1 + p2;
}
// 之所以使用 null 是因為在本例中我們并不關心硬綁定的 this 是什么 
// 反正使用 new 時 this 會被修改
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" ); 
baz.val; // p1p2

綁定規則優先級:new綁定 > 顯式綁定 > 隱式綁定 > 默認綁定

箭頭函數無法使用以上四種綁定規則。

function foo() {
// 返回一個箭頭函數 
  return (a) => {
    //this 繼承自 foo()
    console.log( this.a ); 
  };
}

var obj1 = { 
  a:2
};

var obj2 = { 
  a:3
};

var bar = foo.call( obj1 );
bar.call( obj2 ); // 2

2. 對象

對象的兩種形式定義:聲明(文字)形式和構造形式。

var myObj = { 
  key: value
  // ... 
};

var myObj = new Object(); 
myObj.key = value;

六種主要類型: string,number,boolean,null,undefined,object

object外的5種類型為簡單基本類型,本身并不是對象,但是typeof null會返回字符串 "object"。

內置對象:String,Number,Boolean,Object,Function,Array,Date,RegExp,Error

var strPrimitive = "I am a string"; 
typeof strPrimitive; // "string" 
strPrimitive instanceof String; // false

var strObject = new String( "I am a string" ); 
typeof strObject; // "object"
strObject instanceof String; // true
// 檢查 sub-type 對象
Object.prototype.toString.call( strObject ); // [object String]

在必要時語言會自動把字符串字面量轉換成一個String對象,可以訪問屬性和方法。

對于ObjectArrayFunctionRegExp來說,無論使用文字形式還是構 造形式,它們都是對象,不是字面量。

屬性

屬性名永遠是字符串,雖然在數組下標中使用的的確是數字,但是在對象屬性名中數字會被轉換成字符串。

ES6 增加了可計算屬性名,最常用的場景可能是 ES6 的符號(Symbol)。

var prefix = "foo";
var myObject = {
  [prefix + "bar"]:"hello", 
  [prefix + "baz"]: "world"
};
     
myObject["foobar"]; // hello
myObject["foobaz"]; // world

如果你試圖向數組添加一個屬性,但是屬性名“看起來”像一個數字,那它會變成 一個數值下標

var myArray = [ "foo", 42, "bar" ]; 
myArray["3"] = "baz"; 
myArray.length; // 4
myArray[3]; // "baz"

復制對象

對于JSON安全的對象來說,有一種巧妙的復制方法:

var newObj = JSON.parse( JSON.stringify( someObj ) );

ES6 定義了Object.assign(..)方法來實現淺復制。

屬性描述符

三個特性:writable(可寫)、 enumerable(可枚舉)和 configurable(可配置)。

var myObject = { 
  a:2
};

Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true 
// }

在創建普通屬性時屬性描述符會使用默認值,我們也可以使用 Object.defineProperty(..)來添加一個新屬性或者修改一個已有屬性(如果它是configurable)并對特性進行設置。

var myObject = {};
     Object.defineProperty( myObject, "a", {
         value: 2,
         writable: true, 
         configurable: true, 
         enumerable: true
     } );
     myObject.a; // 2

writable決定是否可以修改屬性的值,如果在嚴格模式下,這 種方法會出錯(TypeError)。

configurable修改成 false 是單向操作,無法撤銷!不管是不是處于嚴格模式,嘗 試修改一個不可配置的屬性描述符都會出錯(TypeError)。

屬性是不可配置時使用 delete也會失敗。

如果把enumerable設置成false,這個屬性就不會出現在枚舉中(比如for..in循環),雖然仍 然可以正常訪問它。

不變性

常量: 結合writable:falseconfigurable:false就可以創建一個真正的常量屬性(不可修改、 重定義或者刪除)

var myObject = {};
     Object.defineProperty( myObject, "FAVORITE_NUMBER", {
         value: 42,
          writable: false,
          configurable: false 
      });

禁止擴展: 如果你想禁止一個對象添加新屬性并且保留已有屬性,可以使用Object.preventExtensions(..)

密封: Object.seal(..) 會創建一個“密封”的對象,這個方法實際上會在一個現有對象上調用Object.preventExtensions(..) 并把所有現有屬性標記為configurable:false

凍結: Object.freeze(..)會創建一個凍結對象,這個方法實際上會在一個現有對象上調用Object.seal(..)并把所有“數據訪問”屬性標記為writable:false,這樣就無法修改它們 的值。

get和set

var myObject = {
// 給 a 定義一個 getter 
  _a:2,
  get a() {
    return this.a; 
  },
// 給 a 定義一個 setter 
  set a(_a){
     this._a = _a;
  }
};

Object.defineProperty( 
  myObject, // 目標對象 
   "b", // 屬性名
  {
  // 描述符
  // 給 b 設置一個 getter
  get: function(){ 
      return this.a * 2 
    },
      // 確保 b 會出現在對象的屬性列表中
     enumerable: true
    }
);

myObject.a; // 2
myObject.b; // 4

在不訪問屬性值的情況下判斷對象中是否存在這個屬性:

var myObject = { 
  a:2
};
 ("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "b" ); // false

in操作符會檢查屬性是否在對象及其 [[Prototype]] 原型鏈中,相比之下,hasOwnProperty(..)只會檢查屬性是否在 myObject 對象中,不會檢查 [[Prototype]] 鏈。

有的對象可能沒有連接到Object.prototype,可以使用Object.prototype.hasOwnProperty. call(myObject,"a")進行判斷。

propertyIsEnumerable(..)會檢查給定的屬性名是否直接存在于對象中(而不是在原型鏈 上)并且滿足enumerable:true

Object.keys(..)會返回一個數組,包含所有可枚舉屬性,Object.getOwnPropertyNames(..)會返回一個數組,包含所有屬性,無論它們是否可枚舉。

數組有內置的@@iterator,因此for..of可以直接應用在數組上。

var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();
it.next(); // { value:1, done:false } 
it.next(); // { value:2, done:false } 
it.next(); // { value:3, done:false } 
it.next(); // { done:true }

手動定義@@iterator:

var myObject = { a: 2,
b: 3 };
Object.defineProperty( myObject, Symbol.iterator, { 
  enumerable: false,
  writable: false,
  configurable: true,
  value: function() { 
      var o = this;
      var idx = 0;
      var ks = Object.keys( o ); 
       return {
          next: function() { 
                return {
                         value: o[ks[idx++]],
                         done: (idx > ks.length)
                     };
        } };
} } );

3. 原型

JavaScript中的對象有一個特殊的 [[Prototype]] 內置屬性,其實就是對于其他對象的引用。幾乎所有的對象在創建時 [[Prototype]] 屬性都會被賦予一個非空的值。

對于默認的 [[Get]] 操作來說,第一步是檢查對象本身是 否有這個屬性,如果有的話就使用它。但是如果不存在與對象本身,就需要會繼續訪問對象的 [[Prototype]] 鏈。

var anotherObject = { 
  a:2
};
// 創建一個關聯到 anotherObject 的對象
var myObject = Object.create( anotherObject ); 
myObject.a; // 2

任何可以通過原型鏈訪問到并且是enumerable的屬性都會被枚舉。

使用in操作符來檢查屬性在對象中是否存在時,同樣會查找對象的整條原型鏈(無論屬性是否可枚舉)。

所有普通的 [[Prototype]] 鏈最終都會指向內置的Object.prototype,它包含 JavaScript中許多通用的功能,比如.toString()

原型鏈上層時myObject.foo = "bar"會出現的三種情況:

  • 如果[[Prototype]]鏈上層存在名為foo的普通數據訪問屬性并且不是只讀,就會直接在 myObject 中添加一個名為 foo 的新 屬性,它是屏蔽屬性。

  • 如果[[Prototype]]鏈上層存在名為foo的普通數據訪問屬性并且只讀,則無法修改已有屬性或者在 myObject 上創建屏蔽屬性。

  • 如果在[[Prototype]]鏈上層存在foo并且它是一個setter,那就一定會 調用這個 setter

有些情況下會隱式產生屏蔽:

var anotherObject = { 
  a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2

anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // 隱式屏蔽! 
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true

++操作首先會通過 [[Prototype]] 查找屬性a并從anotherObject.a獲取當前屬性值2,然后給這個值加1,接著用 [[Put]] 將值3賦給myObject中新建的屏蔽屬性a

所有的函數默認都會擁有一個 名為prototype的公有并且不可枚舉的屬性,它會指向另一個對象,這個對象通常被稱為該對象的原型。

function Foo() {
 // ...
}
Foo.prototype; // { }

在方法射調用new時創建對象時,該對象最后會被關聯到這個方法的prototype對象上。

function Foo() { 
  // ...
}
var a = new Foo();
Object.getPrototypeOf( a ) === Foo.prototype; // true

new Foo()會生成一個新對象,這個新對象的內部鏈接[[Prototype]]關聯的是 Foo.prototype對象。最后我們得到了兩個對象,它們之間互相關聯。

JavaScript中,我們并不會將一個對象(“類”)復制到另一個對象(“實例”),只是將它們關聯起來。這個機制通常被稱為原型繼承。

構造函數

使用new創建的對象會調用類的構造函數。

function Foo() { 
  // ...
}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true

Foo.prototype默認有一個公有并且不可枚舉的屬性.constructor,這個屬性引用的是對象關聯的函數。

可以看到通過“構造函數”調用new Foo()創建的對象也有一個.constructor屬性,指向 “創建這個對象的函數”。

函數本身并不是構造函數,然而,當你在普通的函數調用前面加上new關鍵字之后,就會把這個函數調用變成一個“構造函數 調用”。實際上,new會劫持所有普通函數并用構造對象的形式來調用它。

JavaScript中對于“構造函數”最準確的解釋是,所有帶new的函數調用。

如果 你創建了一個新對象并替換了函數默認的.prototype對象引用,那么新對象并不會自動獲 得.constructor屬性。

function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 創建一個新原型對象
var a1 = new Foo();
a1.constructor === Foo; // false! 
a1.constructor === Object; // true!

可以給 Foo.prototype 添加一個 .constructor 屬性,不過這需要手動添加一個符
合正常行為的不可枚舉屬性。

function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 創建一個新原型對象

Object.defineProperty( Foo.prototype, "constructor" , {
    enumerable: false,
    writable: true,
    configurable: true,
    value: Foo // 讓 .constructor 指向 Foo
});

繼承

典型的“原型風格”:

function Foo(name) { 
  this.name = name;
}
Foo.prototype.myName = function() { 
  return this.name;
};
function Bar(name,label) { 
  Foo.call( this, name ); 
  this.label = label;
}

// 我們創建了一個新的 Bar.prototype 對象并關聯到 Foo.prototype
// 注意!現在沒有 Bar.prototype.constructor 了 
// 如果你需要這個屬性的話可能需要手動修復一下它
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.myLabel = function() { 
  return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"

ES6 開始可以直接修改現有的Bar.prototype

Object.setPrototypeOf( Bar.prototype, Foo.prototype );

檢查一個實例的繼承關系

// 非常簡單:b 是否出現在 c 的 [[Prototype]] 鏈中
b.isPrototypeOf( c );

Object.getPrototypeOf( a ) === Foo.prototype; // true

// 非標準的方法訪問內部 [[Prototype]] 屬性
 a.__proto__ === Foo.prototype; // true

寫了這么多,實在寫不下去了。《你不知道的js》都是滿滿的干貨,筆記記到這里發現好多知識都非常有用,沒辦法省略。幾下這些筆記,也是為了復習一下,以免忘得太快了,所以受益的終究還是自己呀。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容