《你不知道的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》都是滿滿的乾貨,筆記記到這裏發現好多知識都非常有用,沒辦法省略。幾下這些筆記,也是爲了複習一下,以免忘得太快了,所以受益的終究還是自己呀。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章