JavaScript之物件和原型

Miss木不發表於2019-03-15

JavaScript之物件和原型

上篇回顧:

  1. 什麼是作用域? 兩種定義: (1)負責收集並維護由所有宣告的識別符號(變數)組成的一系列查詢,並實施一套非常嚴格的規則,確定當前執行的程式碼對這些識別符號的訪問許可權;(2)執行環境定義了變數或函式有權訪問的其他資料,決定了它們各自的行為。
  2. 詞法作用域(兩種欺騙作用域);
  3. 作用域提升;
  4. 塊級作用域;
  5. 作用域鏈:全域性環境存在全域性執行環境;函式具有自己的執行環境。當程式碼在一個執行環境中執行時,會建立一個變數的作用域鏈,來保證執行環境對所有變數和函式的有序訪問。作用域的前端,始終是當前執行環境的變數物件,作用域的後端,始終是全域性變數物件。
  6. 閉包:簡單來說,閉包是指有權訪問另一個函式作用域中的變數的函式。滿足兩個條件:1.是一個函式;2.能夠訪問另一個函式作用域中的變數。
  7. 通常,函式的作用域及其所有變數都會在函式執行結束後被銷燬。當函式返回了一個閉包時,這個函式的作用域將會一直在記憶體中儲存到閉包不存在為止。
  8. 迴圈閉包問題
  9. 閉包會引起一些問題:this指向改變;記憶體洩漏問題。

一、物件

物件是JavaScript中非常重要的資料結構。

1.基礎回顧

JavaScript基本資料型別:string、number、null、undefined、boolean (ES6中新增了Symbol型別) 補充知識點: ES6 為什麼引入了Symbol?

ES5 的物件屬性名都是字串,這容易造成屬性名的衝突。比如,你使用了一個他人提供的物件,但又想為這個物件新增新的方法(mixin 模式),新方法的名字就有可能與現有方法產生衝突。如果有一種機制,保證每個屬性的名字都是獨一無二的就好了,這樣就從根本上防止屬性名的衝突。這就是 ES6 引入Symbol的原因。

Symbol 值通過Symbol函式生成。這就是說,物件的屬性名現在可以有兩種型別,一種是原來就有的字串,另一種就是新增的 Symbol 型別。凡是屬性名屬於 Symbol 型別,就都是獨一無二的,可以保證不會與其他屬性名產生衝突。 作為屬性名,每一個Symbol值都是不相等的。

// 無引數
let a = Symbol();
let b = Symbol();
a === b //false
// 有引數
let c = Symbol('foo');
let d = Symbol('foo');
c === c //false
複製程式碼

注意:

  • Symbol 值作為物件屬性名時,不能用點運算子。
  • 在物件內部,使用Symbol定義屬性時,Symbol值必須放在方括號[]內。

複雜資料型別:Object * JavaScript的物件是一種無序的集合資料型別,它由若干鍵值對組成。 * 一個JavaScript物件可以有很多屬性,屬性定義了物件的特徵。 * 訪問屬性是通過.操作符完成的,但這要求屬性名必須是一個有效的變數名;物件的屬性也可以通過方括號訪問或者設定。

一個屬性的名稱如果不是一個有效的 JavaScript 識別符號(例如,一個由空格或連字元,或者以數字開頭的屬性名),就只能通過方括號標記訪問。

var myObject = {
  '3': 2
};
myObject.'3'; // Uncaught SyntaxError: Unexpected string
myObject['3']; // 2
複製程式碼

2.物件的內容

物件的內容其實就是物件的屬性,至於物件的屬性到底是怎樣儲存的,實際上儲存在物件容器內部的是這些屬性的名稱,他們就像指標一樣指向屬性值儲存的真正的位置(在後面的JavaScript記憶體機制中會介紹)。在 ES5 之前,JavaScript 語言本身並沒有提供可以直接檢測屬性特性的方法,比如判斷屬性是否是隻讀。但是從 ES5 開始,所有的屬性都具備了屬性描述符。 屬性描述符分為資料描述符和訪問(存取)描述符。

  • 資料描述符 通過getOwnPropertyDescriptor方法可以獲取物件的資料描述符。
var myObject = {
 a:2
};
Object.getOwnPropertyDescriptor( myObject, "a" );
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
複製程式碼

在建立普通物件的時候,資料描述符使用的是預設值(和上面輸出的一樣),但是我們根據自己的需要,可以使用Object.defineProperty()來新增一個新屬性或者修改某個屬性。 value:屬性的值。 writable:是否可寫。當設定為false的時候,表明這個屬性的value是不能被改變的(注意:嚴格模式下,如果writable為false,對屬性進行再次賦值,會報typeError的錯誤!)。 enumerable:是否可列舉。我們有的時候需要對某個物件的屬性進行遍歷,如果enumerable為false,則改屬性不會被遍歷,也就不會出現在列舉中。 configurable: 只有當屬性的configurable為true時,才能通過Object.defineProperty()來改變資料描述符,否則會報typeError錯誤。而且configurable的屬性不能由false改為true。 例外: 修改value的值不受configurable值的影響;當configurable為false時,writable只能由true改為false,不能由false改為true。

delete可以刪除物件的屬性,但是configurable必須為true。

  • 訪問描述符(getter/setter) 讀取屬性使用getter,負責返回有效的值;寫入屬性使用setter,負責處理資料。對於訪問描述來說,JavaScript會忽略他們的value和writable屬性,而是關心set、get、enumerable、configurable屬性。 如何訪問一個屬性? 訪問屬性時,引擎實際上會呼叫內部的預設 [[Get]] 操作(在設定屬性值時是 [[Put]]),[[Get]] 操作會檢查物件本身是否包含這個屬性,如果找到就會返回這個屬性的值,如果沒找到的話還會查詢 [[Prototype]]鏈。

3.列舉一個物件的所有屬性

  • for...in 迴圈:該方法依次訪問一個物件及其原型鏈中所有可列舉的屬性(不含 Symbol 屬性)。
  • Object.keys(obj):該方法返回一個物件 obj 自身包含(不包括原型中)的所有屬性的名稱的陣列。
  • Object.getOwnPropertyNames(obj):該方法返回一個陣列,它包含了物件 obj 所有擁有的屬性(無論是否可列舉)的名稱。
  • Object.getOwnPropertySymbols(obj):返回一個陣列,包含物件自身的所有 Symbol 屬性的鍵名。
  • Reflect.ownKeys(obj)Reflect.ownKeys返回一個陣列,包含物件自身的所有鍵名,不管鍵名是 Symbol 或字串,也不管是否可列舉。

4.建立物件(主要介紹前5種)

  • 物件字面量
let person = {
    name: 'Luna',
    age: '18',
    hobby: 'reading',
    greeting: function() {
        console.log('hello,I am ' + this.name)
    }
};
複製程式碼
  • 使用new表示式
let person = new Object();
person.name = 'Luna';
person.age = '18';
person.hobby = 'reading';
person.greeting = function () {
    console.log('hello,I am ' + this.name)
}
複製程式碼

使用這兩種方式的好處是簡單,但是缺點是如果使用同一個介面建立很多物件,會產生大量的重複程式碼。

  • 使用工廠模式
    函式 createPerson()能夠根據接受的引數來構建一個包含所有必要資訊的 Person 物件。可以無數次地呼叫這個函式,而每次它都會返回一個包含三個屬性一個方法的物件。工廠模式雖然解決了建立 多個相似物件的問題,但卻沒有解決物件識別的問題(即怎樣知道一個物件的型別)。
function ceatePerson(name, age, hobby) {
    let obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.hobby = hobby;
    this.greeting = function () {
    console.log('hello,I am ' + this.name)
    }
    return obj;
}
let person = cratePerson('Luna','18','reading');
複製程式碼
  • 使用建構函式
    使用建構函式有幾個缺點:1.沒有顯示的建立這個物件;2.直接將屬性和方法賦給了 this 物件;3.沒有return語句。
function Person(name, age, hobby) {
    this.name = name;
    this.age = age;
    this.hobby = hobby;
    this.greeting = function () {
    console.log('hello,I am ' + this.name)
    }
}
let person = new Person('Luna','18','reading');
複製程式碼

補充知識點:
什麼是建構函式?
建構函式本身就是一個函式,與普通函式沒有任何區別,不過為了規範一般將其首字母大寫。建構函式和普通函式的唯一區別在於呼叫方式的不同,使用 new 生成例項的函式就是建構函式,直接呼叫的就是普通函式。constructor 返回建立例項物件時建構函式的引用。

建構函式的缺點:每個方法都要在每個例項上重新建立一遍。

  • 原型模式
    我們建立的每個函式都有一個 prototype(原型)屬性,這個屬性是一個指標,指向一個物件,而這個物件的用途是包含可以由特定型別的所有例項共享的屬性和方法。如果按照字面意思來理解,那 麼 prototype 就是通過呼叫建構函式而建立的那個物件例項的原型物件。使用原型物件的好處是可以讓所有物件例項共享它所包含的屬性和方法。換句話說,不必在建構函式中定義物件例項的資訊,而是 可以將這些資訊直接新增到原型物件中,
function Person () {}
Person.prototype.name = 'Luna';
Person.prototype.age = '18';
Person.prototype.hobby = 'reading';
Person.prototype.greeting = function () {
     console.log('hello,I am ' + this.name)
}
let person = new Person()
複製程式碼
  • 組合使用建構函式模式和原型模式
function Person (name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}
Person.prototype = {
    constructor : Person,
    sayName : function () {
        alert(this.name);
    }
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
複製程式碼
  • 動態原型模式
function Person(name, age, job){
    //屬性
    this.name = name;
    this.age = age;
    this.job = job;
    //方法
    if (typeof this.sayName != "function") {
        Person.prototype.sayName = function(){
            alert(this.name);
        };
    }
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
複製程式碼
  • 寄生建構函式模式
function Person (name, age, job) {
 var o = new Object();
 o.name = name;
 o.age = age;
 o.job = job;
 o.sayName = function(){
 alert(this.name);
 };
 return o;
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
複製程式碼
  • 穩妥建構函式模式
function SpecialArray () {
    //建立陣列
    var values = new Array();
    //新增值
    values.push.apply(values, arguments);
    //新增方法
    values.toPipedString = function(){
    return this.join("|");
};

 //返回陣列
 return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"
複製程式碼

5.物件的方法

  1. Object.is():ES6 提出“Same-value equality”(同值相等)演算法(ES5:JavaScript 缺乏一種運算,在所有環境中,只要兩個值是一樣的,它們就應該相等。),用Object.is()解決嚴格相等的問題。

與"==="的區別:

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
複製程式碼
  1. Object.assign():通過複製一個或多個物件來建立一個新的物件(屬於淺拷貝【Object.assign()淺拷貝在後面章節會進行講解】)。
var target = { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };

Object.assign(target, source1, source2);
複製程式碼
  1. Object.create():會建立一個物件並把這個物件的 [[Prototype]] 關聯到指定的物件。
var person = {
    name: 'Luna',
    age: '18',
    hobby: 'reading',
    greeting: function() {
        console.log('hello,I am ' + this.name)
    }
};

var me = Object.create(person);
me.name = 'Bella';
複製程式碼

點選這裡檢視Object的方法

二、原型

1.基礎回顧

JavaScript也是物件導向的,而物件導向的一個重要的方法就是繼承。A物件通過繼承B物件,就可以擁有B物件的所有屬性和方法。我們知道,java是通過類來是實現的,但是對於JavaScript來說,在ES6以前,是通過原型來實現的(ES6提出了class)。

建構函式、原型和例項的關係: 每個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標,而例項都包含一個指向原型物件的內部指標。

  • prototype 只有函式擁有這個屬性,指向一個物件。對於建構函式來說,生成例項的時候,該屬性會自動成為例項物件的原型。原型物件的屬性不是例項物件自身的屬性。只要修改原型物件,變動就立刻會體現在所有例項物件上。原型物件的作用,就是定義所有例項物件共享的屬性和方法。

  • __proto__
    這是每個物件都有的隱式原型屬性,指向了建立該物件的建構函式的原型。當我們使用new操作符時,生成的例項就有了__proto__屬性。

let fun = Function.prototype.bind()
複製程式碼
function Person(name, hobby) {
    this.name = name;
    this.hobby = hobby;
    this.greeting = function () {
    console.log('hello,I am ' + this.name)
    }
}

let person = new Person('Luna','reading');

Person.prototype; // constructor
Person.prototype.constructor; // Person()
person.__proto__;// constructor

Person.prototype.age = 18;
person.name; // Luna
person.age; // 18
複製程式碼

2.原型鏈

每個物件擁有一個原型物件,通過 __proto__ 指標指向上一個原型 ,並從中繼承方法和屬性,同時原型物件也可能擁有原型,這樣一層一層,最終指向 null。這種關係被稱為原型鏈 (prototype chain),通過原型鏈一個物件會擁有定義在其他物件中的屬性和方法。

原型鏈

當我們訪問一個物件的屬性時,引擎會先呼叫內建的[GET]方法,檢查物件本身是否有該屬性;如果沒有,再找他的原型物件,如果沒有找到,再一層議程往上找他的原型,原型的盡頭是null。 在這張原型鏈圖中,有幾點需要注意:Object()也屬於建構函式,所以他的原型物件是Function.prototype.

3.經典面試題之手寫一個new實現

從上面的例子中,我們可以看出,new建立的例項即可以訪問到其建構函式裡的屬性,也可以訪問到原型裡的屬性。

當程式碼 new Foo(...) 執行時,會發生以下事情:
一個繼承自 Foo.prototype 的新物件被建立。
使用指定的引數呼叫建構函式 Foo ,並將 this 繫結到新建立的物件。new Foo 等同於 new Foo(),也就是沒有指定引數列表,Foo 不帶任何引數呼叫的情況。 由建構函式返回的物件就是 new 表示式的結果。如果建構函式沒有顯式返回一個物件,則使用步驟1建立的物件。

function create() {
	// 建立一個空的物件
    var obj = new Object(),
	// 獲得建構函式,arguments中去除第一個引數
    Con = [].shift.call(arguments);
	// 連結到原型,obj 可以訪問到建構函式原型中的屬性
    obj.__proto__ = Con.prototype;
	// 繫結 this 實現繼承,obj 可以訪問到建構函式中的屬性
    // var ret = Con.apply(obj, arguments);
    var ret = Object.create(obj);
	// 優先返回建構函式返回的物件
	return ret instanceof Object ? ret : obj;
};
複製程式碼

我們來測試一下:

// 測試用例
function Car(color) {
    this.color = color;
}
Car.prototype.start = function() {
    console.log(this.color + " car start");
}

var car = create(Car, "black");
car.color;
// black

car.start();
// black car start

複製程式碼

三、總結

  1. 物件是7個基礎型別之一;
  2. 物件是鍵值對的集合,可以通過.和[]獲取屬性值;
  3. 屬性的特性可以通過屬性描述符來控制;
  4. 屬性不一定包含值,可能是具備 getter/setter 的“訪問描述符”;
  5. 可以使用for in遍歷(一共有五種方式)屬性名,使用for of遍歷屬性的值;
  6. 建立物件的幾種常見方法:字面物件;new表示式;工廠模式;建構函式模式;原型模式;
  7. 每個物件例項都對應一個原型物件,當我們訪問一個物件的屬性時,會先觸發內建的[GET]方法,找該物件是否有此屬性,如果沒有,會找他的原型物件,原型物件沒有,再沿著原型鏈一層一層往上找,知道原型鏈的盡頭--null,最後沒有找到,會返回undefined;
  8. 如何手寫一個new實現。

參考:
ECMAScript 6 入門 --阮一峰
Object|MDN 深度解析new原理及模擬實現
《你不知道的JavaScript--上》
《JavaScript高階程式設計--第三版》

相關文章