【JS 口袋書】第 5 章:JS 對象生命週期的祕密

做者:valentinogagliardihtml

譯者:前端小智前端

來源:githubgit


阿里雲最近在作活動,低至2折,有興趣能夠看看: promotion.aliyun.com/ntms/yunpar…github


爲了保證的可讀性,本文采用意譯而非直譯。編程

一切皆對象

我們常常聽到JS中「一切皆對象」? 有沒有問想過這是什麼意思? 其它語言也有「一切皆對象」之說,如Python。 可是Python中的對象不只僅是像JS對象這樣的存放值和值的容器。 Python中的對象是一個。 JS中有相似的東西,但JS中的「對象」只是鍵和值的容器:json

var obj = { name: "Tom", age: 34 }
複製代碼

實際上,JS中的對象是一種「啞」類型,但不少其餘實體彷佛都是從對象派生出來的。 甚至是數組,在JS中建立一個數組,以下所示:數組

var arr = [1,2,3,4,5]
複製代碼

而後用typeof運算符檢查類型,會看到一個使人驚訝的結果:瀏覽器

typeof arr
"object"
複製代碼

看來數組是一種特殊的對象! 即便JS中的函數也是對象。 若是你深刻挖掘,還有更多,建立一個函數,該函數就會附加一些方法:微信

var a = function(){ return false; }
a.toString()
複製代碼

輸出:編程語言

"function(){ return false; }"
複製代碼

我們並無在函數聲明toString方法,因此在底層必定還有東西。它從何而來? Object有一個名爲.toString的方法。 彷佛我們的函數具備相同的Object方法。

Object.toString()
複製代碼

這時我們使用瀏覽器控制檯來查看默認被附加的函數和屬性,這個謎團就會變得更加複雜:

誰把這些方法放在函數呢。 JS中的函數是一種特殊的對象,這會不會是個暗示? 再看看上面的圖片:咱們的函數中有一個名爲prototype的奇怪命名屬性,這又是什麼鬼?

JS中的prototype是一個對象。 它就像一個揹包,附着在大多數JS內置對象上。 例如 Object, Function, Array, Date, Error,都有一個「prototype」:

typeof Object.prototype // 'object'
typeof Date.prototype // 'object'
typeof String.prototype // 'object'
typeof Number.prototype // 'object'
typeof Array.prototype // 'object'
typeof Error.prototype // 'object'
複製代碼

注意內置對象有大寫字母:

  • String
  • Number
  • Boolean
  • Object
  • Symbol
  • Null
  • Undefined

如下除了Object是類型以外,其它是JS的基本類型。另外一方面,內置對象就像JS類型的鏡像,也用做函數。例如,可使用String做爲函數將數字轉換爲字符串:

String(34)
複製代碼

如今回到「prototype」。prototype是全部公共方法和屬性的宿主,從祖先派生的「子」對象能夠從使用祖先的方法和屬性。也就是說,給定一個原始 prototype,我們能夠建立新的對象,這些對象將使用一個原型做爲公共函數的真實源,不 Look see see。

假設有個要求建立一個聊天應用程序,有我的物對象。這我的物能夠發送消息,登陸時,會收到一個問候。

根據需求我們很容易定義這個麼一 Person 對象:

var Person = {
  name: "noname",
  age: 0,
  greet: function() {
    console.log(`Hello ${this.name}`);
  }
};
複製代碼

你可能會想知道,爲何這裏要使用字面量的方式來聲明 Person 對象。 稍後會詳細說明,如今該 Person「模型」。經過這個模型,我們使用 Object.create() 來建立覺得這個模型爲基礎的對象。

建立和連接對象

JS中對象彷佛以某種方式連接在一塊兒,Object.create()說明了這一點,此方法從原始對象開始建立新對象,再來建立一個新Person 對象:

var Person = {
  name: "noname",
  age: 0,
  greet: function() {
    console.log(`Hello ${this.name}`);
  }
};

var Tom = Object.create(Person);
複製代碼

如今,Tom 是一個新的對象,可是我們沒有指定任何新的方法或屬性,但它仍然能夠訪問Person中的nameage 屬性。

var Person = {
  name: "noname",
  age: 0,
  greet: function() {
    console.log(`Hello ${this.name}`);
  }
};

var Tom = Object.create(Person);

var tomAge = Tom.age;
var tomName = Tom.name;

console.log(`${tomAge} ${tomName}`);

// Output: 0 noname
複製代碼

如今,能夠從一個共同的祖先開始建立新的person。但奇怪的是,新對象仍然與原始對象保持鏈接,這不是一個大問題,由於「子」對象能夠自定義屬性和方法

var Person = {
  name: "noname",
  age: 0,
  greet: function() {
    console.log(`Hello ${this.name}`);
  }
};

var Tom = Object.create(Person);

Tom.age = 34;
Tom.name = "Tom";
var tomAge = Tom.age;
var tomName = Tom.name;

console.log(`${tomAge} ${tomName}`);

// Output: 34 Tom
複製代碼

這種方式被稱爲「屏蔽」原始屬性。 還有另外一種將屬性傳遞給新對象的方法。 Object.create將另外一個對象做爲第二個參數,能夠在其中爲新對象指定鍵和值:

var Tom = Object.create(Person, {
  age: {
    value: 34
  },
  name: {
    value: "Tom"
  }
});
複製代碼

以這種方式配置的屬性默認狀況下不可寫,不可枚舉,不可配置。 不可寫意味着以後沒法更改該屬性,更改會被忽略:

var Tom = Object.create(Person, {
  age: {
    value: 34
  },
  name: {
    value: "Tom"
  }
});

Tom.age = 80;
Tom.name = "evilchange";

var tomAge = Tom.age;
var tomName = Tom.name;

Tom.greet();

console.log(`${tomAge} ${tomName}`);

// Hello Tom
// 34 Tom
複製代碼

不可枚舉意味着屬性不會在 for...in 循環中顯示,例如:

for (const key in Tom) {
  console.log(key);
}

// Output: greet
複製代碼

可是正如我們所看到的,因爲JS引擎沿着原型鏈向上查找,在「父」對象上找到greet屬性。最後,不可配置意味着屬性既不能修改也不能刪除。

Tom.age = 80;
Tom.name = "evilchange";
delete Tom.name;
var tomAge = Tom.age;
var tomName = Tom.name;

console.log(`${tomAge} ${tomName}`);

// 34 Tom
複製代碼

若是要更改屬性的行爲,只需配writable(可寫性),configurable(可配置),enumerable(可枚舉)屬性便可。

var Tom = Object.create(Person, {
  age: {
    value: 34,
    enumerable: true,
    writable: true,
    configurable: true
  },
  name: {
    value: "Tom",
    enumerable: true,
    writable: true,
    configurable: true
  }
});
複製代碼

如今,Tom也能夠經過如下方式訪問greet()

var Person = {
  name: "noname",
  age: 0,
  greet: function() {
    console.log(`Hello ${this.name}`);
  }
};

var Tom = Object.create(Person);

Tom.age = 34;
Tom.name = "Tom";
var tomAge = Tom.age;
var tomName = Tom.name;
Tom.greet();

console.log(`${tomAge} ${tomName}`);

// Hello Tom
// 34 Tom
複製代碼

暫時不要過於擔憂「this」。 拉下來會詳細介紹。暫且先記住,「this」是對函數執行的某個對象的引用。在我們的例子中,greet()Tom的上下文中運行,所以能夠訪問「this.name」。

構建JavaScript對象

目前爲止,只介紹了關於「prototype」的一點知識 ,還有玩了一會 Object.create()以外但我們沒有直接使用它。 隨着時間的推移出現了一個新的模式:構造函數。 使用函數建立新對象聽起來很合理, 假設你想將Person對象轉換爲函數,你能夠用如下方式:

function Person(name, age) {
  var newPerson = {};
  newPerson.age = age;
  newPerson.name = name;
  newPerson.greet = function() {
    console.log("Hello " + newPerson.name);
  };
  return newPerson;
}
複製代碼

所以,不須要處處調用object.create(),只需將Person做爲函數調用:

var me = Person("Valentino");
複製代碼

構造函數模式有助於封裝一系列JS對象的建立和配置。 在這裏, 我們使用字面量的方式建立對象。 這是一種從面嚮對象語言借用的約定,其中類名開頭要大寫。

上面的例子有一個嚴重的問題:每次我們建立一個新對象時,一遍又一遍地重複建立greet()函數。可使用Object.create(),它會在對象之間建立連接,建立次數只有一次。 首先,我們將greet()方法移到外面的一個對象上。 而後,可使用Object.create()將新對象連接到該公共對象:

var personMethods = {
  greet: function() {
    console.log("Hello " + this.name);
  }
};

function Person(name, age) {
  // greet lives outside now
  var newPerson = Object.create(personMethods);
  newPerson.age = age;
  newPerson.name = name;
  return newPerson;
}

var me = Person("Valentino");
me.greet();

// Output: "Hello Valentino"
複製代碼

這種方式比剛開始會點,還能夠進一步優化就是使用prototypeprototype是一個對象,能夠在上面擴展屬性,方法等等。

Person.prototype.greet = function() {
  console.log("Hello " + this.name);
};
複製代碼

移除了personMethods。 調整Object.create的參數,不然新對象不會自動連接到共同的祖先:

function Person(name, age) {
  // greet lives outside now
  var newPerson = Object.create(Person.prototype);
  newPerson.age = age;
  newPerson.name = name;
  return newPerson;
}

Person.prototype.greet = function() {
  console.log("Hello " + this.name);
};

var me = Person("Valentino");
me.greet();

// Output: "Hello Valentino"
複製代碼

如今公共方法的來源是Person.prototype。 使用JS中的new運算符,能夠消除Person中的全部噪聲,而且只須要爲this分配參數。

下面代碼:

function Person(name, age) {
  // greet lives outside now
  var newPerson = Object.create(Person.prototype);
  newPerson.age = age;
  newPerson.name = name;
  return newPerson;
}
複製代碼

改爲:

function Person(name, age) {
  this.name = name;
  this.age = age;
}
複製代碼

完整代碼:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log("Hello " + this.name);
};

var me = new Person("Valentino");
me.greet();

// Output: "Hello Valentino"
複製代碼

注意,使用new關鍵字,被稱爲「構造函數調用」new 幹了三件事情

  • 建立一個空對象

  • 將空對象的__proto__指向構造函數的prototype

  • 使用空對象做爲上下文的調用構造函數

    function Person(name, age) { this.name = name; this.age = age; }

根據上面描述的,new Person("Valentino") 作了:

  • 建立一個空對象:var obj = {}
  • 將空對象的__proto__指向構造函數的 prototype:obj.__proto__ = Person().prototype
  • 使用空對象做爲上下文調用構造函數: Person.call(obj)

檢查原型鏈

檢查JS對象之間的原型連接有不少種方法。 例如,Object.getPrototypeOf是一個返回任何給定對象原型的方法。 考慮如下代碼:

var Person = {
  name: "noname",
  age: 0,
  greet: function() {
    console.log(`Hello ${this.name}`);
  }
};

var Tom = Object.create(Person);
複製代碼

檢查Person是不是Tom的原型:

var tomPrototype = Object.getPrototypeOf(Tom);

console.log(tomPrototype === Person);

// Output: true
複製代碼

固然,若是使用構造函數調用構造對象,Object.getPrototypeOf也能夠工做。 可是應該檢查原型對象,而不是構造函數自己:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log("Hello " + this.name);
};

var me = new Person("Valentino");

var mePrototype = Object.getPrototypeOf(me);

console.log(mePrototype === Person.prototype);

// Output: true
複製代碼

除了Object.getPrototypeOf以外,還有另外一個方法isPrototypeOf。 該方法用於測試一個對象是否存在於另外一個對象的原型鏈上,以下所示,檢查 me 是否在 Person.prototype 上:

Person.prototype.isPrototypeOf(me) && console.log('Yes I am!')
複製代碼

instanceof運算符也能夠用於測試構造函數的prototype屬性是否出如今對象的原型鏈中的任何位置。 老實說,這個名字有點誤導,由於JS中沒有「實例」。 在真正的面嚮對象語言中,實例是從類建立的新對象。 請考慮Python中的示例。 我們有一個名爲Person的類,我們從該類建立一個名爲「tom」的新實例:

class Person():
    def __init__(self, age, name):
        self.age = age;
        self.name = name;

    def __str__(self):
        return f'{self.name}'
        

tom = Person(34, 'Tom')
複製代碼

注意,在Python中沒有new關鍵字。如今,我們可使用isinstance方法檢查tom是不是Person的實例

isinstance(tom, Person)

// Output: True
複製代碼

Tom也是Python中「object」的一個實例,下面的代碼也返回true

isinstance(tom, object)

// Output: True
複製代碼

根據isinstance文檔,「若是對象參數是類參數的實例,或者是它的(直接、間接或虛擬)子類的實例,則返回true」。我們在這裏討論的是類。如今讓我們看看instanceof作了什麼。我們將從JS中的Person函數開始建立tom(由於沒有真正的類)

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  console.log(`Hello ${this.name}`);
};

var tom = new Person(34, "Tom");
複製代碼

使用isinstance方法檢查tom是不是PersonObject 的實例

if (tom instanceof Object) {
  console.log("Yes I am!");
}

if (tom instanceof Person) {
  console.log("Yes I am!");
}
複製代碼

所以,能夠得出結論:JS對象的原型老是鏈接到直接的「父對象」和Object.prototype。沒有像PythonJava這樣的類。JS是由對象組成,那麼什麼是原型鏈呢?若是你注意的話,我們提到過幾回「原型鏈」。JS對象能夠訪問代碼中其餘地方定義的方法,這看起來很神奇。再次考慮下面的例子:

var Person = {
  name: "noname",
  age: 0,
  greet: function() {
    console.log(`Hello ${this.name}`);
  }
};

var Tom = Object.create(Person);

Tom.greet();
複製代碼

即便該方法不直接存在於「Tom」對象上,Tom也能夠訪問greet()

這是JS的一個內在特徵,它從另外一種稱爲Self的語言中借用了原型系統。 當訪問greet()時,JS引擎會檢查該方法是否可直接在Tom上使用。 若是不是,搜索將繼續向上連接,直到找到該方法。

「鏈」是Tom鏈接的原型對象的層次結構。 在咱們的例子中,TomPerson類型的對象,所以Tom的原型鏈接到Person.prototype。 而Person.prototypeObject類型的對象,所以共享相同的Object.prototype原型。 若是在Person.prototype上沒有greet(),則搜索將繼續向上連接,直到到達Object.prototype。 這就是我們所說的**「原型鏈」**。

保護對象不受操縱

大多數狀況下,JS 對象「可擴展」是必要的,這樣我們能夠向對象添加新屬性。 但有些狀況下,咱們但願對象不受進一步操縱。 考慮一個簡單的對象:

var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};
複製代碼

默認狀況下,每一個人均可以向該對象添加新屬性

var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};

superImportantObject.anotherProperty = "Hei!";

console.log(superImportantObject.anotherProperty); // Hei!
複製代碼

Object.preventExtensions()方法讓一個對象變的不可擴展,也就是永遠不能再添加新的屬性。

var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};

Object.preventExtensions(superImportantObject);

superImportantObject.anotherProperty = "Hei!";

console.log(superImportantObject.anotherProperty); // undefined
複製代碼

這種技術對於「保護」代碼中的關鍵對象很是方便。JS 中還有許多預先建立的對象,它們都是爲擴展而關閉的,從而阻止開發人員在這些對象上添加新屬性。這就是「重要」對象的狀況,好比XMLHttpRequest的響應。瀏覽器供應商禁止在響應對象上添加新屬性

var request = new XMLHttpRequest();
request.open("GET", "https://jsonplaceholder.typicode.com/posts");
request.send();
request.onload = function() {
  this.response.arbitraryProp = "我是新添加的屬性";
  console.log(this.response.arbitraryProp); // undefined
};
複製代碼

這是經過在「response」對象上內部調用Object.preventExtensions來完成的。 您還可使用Object.isExtensible方法檢查對象是否受到保護。 若是對象是可擴展的,它將返回true

var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};

Object.isExtensible(superImportantObject) && console.log("我是可擴展的");
複製代碼

若是對象不可擴展的,它將返回false

var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};

Object.preventExtensions(superImportantObject);

Object.isExtensible(superImportantObject) ||
  console.log("我是不可擴展的!");
複製代碼

固然,對象的現有屬性能夠更改甚至刪除

var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};

Object.preventExtensions(superImportantObject);

delete superImportantObject.property1;

superImportantObject.property2 = "yeees";

console.log(superImportantObject); // { property2: 'yeees' }
複製代碼

如今,爲了防止這種操做,能夠將每一個屬性定義爲不可寫和不可配置。爲此,有一個方法叫Object.defineProperties

var superImportantObject = {};

Object.defineProperties(superImportantObject, {
  property1: {
    configurable: false,
    writable: false,
    enumerable: true,
    value: "some string"
  },
  property2: {
    configurable: false,
    writable: false,
    enumerable: true,
    value: "some other string"
  }
});
複製代碼

或者,更方便的是,能夠在原始對象上使用Object.freeze

var superImportantObject = {
  property1: "some string",
  property2: "some other string"
};

Object.freeze(superImportantObject);
複製代碼

Object.freeze工做方式與Object.preventExtensions相同,而且它使全部對象的屬性不可寫且不可配置。 惟一的缺點是「Object.freeze」僅適用於對象的第一級:嵌套對象不受操做的影響。

class

有大量關於ES6 類的文章,因此在這裏只討論幾點。JS是一種真正的面嚮對象語言嗎?看起來是這樣的,若是我們看看這段代碼

class Person {
  constructor(name) {
    this.name = name;
  }

  greet() {
    console.log(`Hello ${this.name}`);
  }
}
複製代碼

語法與Python等其餘編程語言中的類很是類似:

class Person:
    def __init__(self, name):
        self.name = name
    
    def greet(self):
        return 'Hello' + self.name
複製代碼

或 PHP

class Person {
    public $name; 

    public function __construct($name){
        $this->name = $name;
    }

    public function greet(){
        echo 'Hello ' . $this->name;
    }
}
複製代碼

ES6中引入了類。可是在這一點上,我們應該清楚JS中沒有「真正的」類。 一切都只是一個對象,儘管有關鍵字class,「原型系統」仍然存在。 新的JS版本是向後兼容的,這意味着在現有功能的基礎上添加了新功能,這些新功能中的大多數都是遺留代碼的語法糖。

總結

JS中的幾乎全部東西都是一個對象。 從字面上看。 JS對象是鍵和值的容器,也可能包含函數。 Object是JS中的基本構建塊:所以能夠從共同的祖先開始建立其餘自定義對象。 而後我們能夠經過語言的內在特徵將對象連接在一塊兒:原型系統。

從公共對象開始,能夠建立共享原始「父」的相同屬性和方法的其餘對象。 可是它的工做方式不是經過將方法和屬性複製到每一個孩子,就像OOP語言那樣。 在JS中,每一個派生對象都保持與父對象的鏈接。 使用Object.create或使用所謂的構造函數建立新的自定義對象。 與new關鍵字配對,構造函數相似於模仿傳統的OOP類。

思考

  • 如何建立不可變的 JS 對象?
  • 什麼是構造函數調用?
  • 什麼是構造函數?
  • 「prototype」 是什麼?
  • 能夠描述一下 new 在底層下作了哪些事嗎?

代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具 Fundebug

原文:github.com/valentinoga…

交流(歡迎加入羣,羣工做日都會發紅包,互動討論技術)

阿里雲最近在作活動,低至2折,有興趣能夠看看:promotion.aliyun.com/ntms/yunpar…

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

github.com/qq449245884…

由於篇幅的限制,今天的分享只到這裏。若是你們想了解更多的內容的話,能夠去掃一掃每篇文章最下面的二維碼,而後關注我們的微信公衆號,瞭解更多的資訊和有價值的內容。

clipboard.png

每次整理文章,通常都到2點才睡覺,一週4次左右,挺苦的,還望支持,給點鼓勵

相關文章
相關標籤/搜索