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

前端小智發表於2019-10-14

作者:valentinogagliardi

譯者:前端小智

來源:github


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


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

一切皆物件

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

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 口袋書】第 5 章:JS 物件生命週期的祕密

誰把這些方法放在函式呢。 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次左右,挺苦的,還望支援,給點鼓勵

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

相關文章