首先要明確的是,JavaScript 是面向對象的語言,但與 Java 、 C# 等語言有別,沒有類的概念,而是基於原型鏈 (即使是ES6的"class"也是基於原型鏈的一種語法糖)。
理解原型對象
只要創建一個新函數,系統默認爲其創建一個 prototype 屬性,指向函數的原型對象。默認情況下,所有原型對象都自動獲得一個 constructor 屬性,指向prototype 屬性所在函數得指針。舉個簡單例子:
function Person(){
this.prop = 'myProp'
}
Person.prototype.name = 'zhangsan';
Person.prototype.sayName = function(){
return this.name
}
var person1 = new Person(); // {prop:myProp}
var person2 = new Person(); // {prop:myProp}
console.log(person1.sayName); // 'zhangsan'
person1.sayName = 'lisi'; // 修改原型對象上屬性
console.log(person1.sayName); // 'lisi' 證明變量是共享的
console.log(Person.prototype.constructor === Person); // true
console.log(person1.__proto__ === Person.prototype); // true
上述例子簡單證明了原型鏈內的一些規則:
- 使用 "new" 運算符時,實際有幾步操作:
- 創建一個空對象;
- 爲對象添加 "prop" 屬性;
- 爲對象的 " [[prototype]] " 屬性指向 "Person.prototype"
- "Person.prototype"爲實例對象"person1"和"person2"提供了屬性和方法,且實例對象能對原型上的屬性方法做修改;
- "Person.prototype"的"constructor"屬性指向原型函數;
- 實例對象的"[[prototype]]"屬性(即__proto__)指向"Person.prototype"。用一個圖表示:
更簡單的原型語法
上述例子中,如果要對"Person.prototype"添加多個屬性方法,大可不必一個個列出,因爲"prototype"屬性指向的就是一個對象,可用如下語法寫:
function Person(){}
Person.prototype = {
name: 'zhangsan',
age: 24,
job: 'dazha',
// ...
}
// 直接賦值方式會導致 prototype 中默認提供的 constructor 屬性消失,可顯示指定
Person.prototype.constructor = Person
但這種方式設定的"constructor"值的[[Enumrable]]爲true,建議使用Object.defineProperty()。
判斷/獲取對象的原型對象
繼續以上面例子,有兩種方法可以判斷對象的原型:
- Object.getPrototypeOf() 靜態方法
var Person_prototype = Object.getPrototypeOf(person1);
console.log(Person.prototype === Person_prototype); // true
- instanceof 運算符
console.log(person1 instanceof Person); // true
原型鏈
關於原型鏈的定義不多說,大概就是子對象能從父對象讀取到屬性和方法。先看一個簡單例子:
function Parent(){
this.age = 50;
}
Parent.prototype.name = 'Parent';
Parent.prototype.getAge = function(){
return this.age
}
function Son(){
this.age = 24;
}
// 創建匿名對象用於繼承
Son.prototype = new Parent()
let son = new Son();
console.log(parent.getAge()); // 24
上述例子中,"son"從"parent"獲取"getAge"方法,用於返回自身"age"屬性的值,期間大概經歷瞭如下幾步:
- 從自身尋找 "getAge" 方法,未找到則從 "__proto__" 往上找, "__proto__" 指向匿名對象 "new Parent()" ;
- 從匿名對象中找 "getAge" 方法,未找到則從 "__proto__" 往上找, "__proto__" 指向 "Parent.prototype" ;
- 從 "Parent.prototype" 找 "getAge" 方法,找到並執行,函數作用域是 "son" 對象,所以返回 "24" 。
上述例子的關係圖如下:
如果是"更長"的原型鏈,是同樣的原理,直到最後指向"null"後停止。
原型鏈的簡單創造
1. 使用 "Object.create(<proto>, <propertiesObject>?)" 方法可簡化上例子,傳入參數有兩個:(具體用法查看MDN文檔)
- proto:新創建對象的原型對象。
- propertiesObject:可選。如果沒有指定爲 undefined,則是要添加到新創建對象的可枚舉屬性(即其自身定義的屬性,而不是其原型鏈上的枚舉屬性)對象的屬性描述符以及相應的屬性名稱。
let parent = {
age: 50,
getAge() {
return this.age
}
}
let son = Object.create(parent, {
// 屬性構造
age: {
value: 24
}
});
console.log(son.age); // 24
2. 使用 "Object.setPrototypeOf(obj, prototype)" 方法:
- obj:作爲"子"的對象
- prototype:作爲"父"的對象
let obj1 = { a: 1 }
let obj2 = { b: 2 }
Object.setPrototypeOf(obj1, obj2); // {a:1}
console.log(obj1.b); // 2
ES6 的 "class" 關鍵字
ES6的"class"關鍵字本質是ES5原型鏈的語法糖,更詳細介紹請看 阮一峯的ES6 。
class Parent {
// 父類構造函數
constructor(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname
}
say() {
return 'my name is '+this.lastname + this.firstname
}
// 靜態方法
static toString() {
console.log('Parent static method')
}
}
// 子類,使用"extents"關鍵字繼承
class Son extends Parent{
constructor(firstname, lastname, age){
// 調用"super"方法前無法使用"this"
super(firstname, lastname)
this._age = age;
}
say(){
// "super"可獲取父類的方法
let result = super.say()
return result + '. my age is ' + this._age
}
// getter方法,setter同理
get age(){
return this._age
}
// 同名靜態方法,當此方法未定義時,會調用父類的靜態方法
static toString() {
console.log('Son static method')
}
}
let parent = new Parent('san', 'zhang'); // "new"創建實例對象
console.log(parent.say()); // 'my name is zhangsan'
Parent.toString(); // 'Parent static method'
let son = new Son('ergou','zhang',16); // 子類的實例
console.log(son.say()); // 'my name is zhangergou. my age is 16'
Son.toString(); // 'Son static method'
console.log(son.age); // 16
"class" 有如下特徵:
- "class" 必須使用 "new" 運算符創建實例,直接調用(如 "Parent()" )會報錯;
- "constructor" 構造函數,在創建實例對象時提供屬性/方法;
- 構造函數外部(如 "sayName" )是實例對象的原型方法,即所有繼承自"Parent"的實例對象都可以訪問的方法;
- 可以創建 "getter" 和 "setter" ;
- 和函數一樣,具有 "name" 屬性,可以獲取類名;
- 和函數一樣,可以使用表達式形式定義,如 "const MyClass = class Me {...}" ;
- 內部可以定義 Generator 方法;
- "static" 定義靜態方法,可以被子類繼承,該方法不會被實例繼承,而是直接通過類來調用;
- "extends" 實現繼承,子類可以獲得父類的所有屬性和方法;
- 構造函數內必須調用 "super()" 方法,否則無法使用 "this" 關鍵字;
- 除了構造函數內,其他函數均可調用 "super" 屬性,指向父類;
- ......
原型鏈存在的問題
上例子可以看出,實例對象"person1" 和 "person2" 均能訪問原型對象上的屬性方法,但是實例對象卻能對其修改,導致所有實例對象均產生影響。解決辦法是,在實例對象上定義同名屬性,則會優先調用實例對象上的屬性。