JavaScript 中的面向對象
本文轉載自微信公眾號「勾勾的前端世界」,作者西嶺。轉載本文請聯系勾勾的前端世界公眾號。
回憶一下什么是對象:Coding 第一奧義:面向對象編程
JavaScript 語言本身的設計缺陷,誤打誤撞,成了解釋最為徹底的“世界原本的樣子”的計算機編程語言;
——西嶺《凡人凡語》
Everything is object (萬物皆對象),JS 語言中將一切都視為 對象 。
JavaScript 語言的對象體系,不基于“類” 創建對象,是基于構造函數(constructor)和原型鏈(prototype)。
簡單方式創建對象
我們可以直接通過 new Object() 創建:
- var person = new Object()
- person.name = 'Jack'
- person.age = 18
- person.sayName = function () {
- console.log(this.name)
- }
字面量方式創建對象
每次創建通過 new Object() 比較麻煩,所以可以通過它的簡寫形式對象字面量來創建:
- var person = {
- name: 'Jack',
- age: 18,
- sayName: function () {
- console.log(this.name)
- }
- }
構造函數
JavaScript 語言使用構造函數作為對象的模板。
所謂 "構造函數",就是一個普通的函數,只不過我們專門用它來生成對象,這樣使用的函數,就是構造函數。
它提供模板,描述對象的基本結構。一個構造函數,可以生成多個對象,這些對象都有相同的結構。
- function Person (name, age) {
- this.name = name
- this.age = age
- this.sayName = function () {
- console.log(this.name)
- }
- }
- var p1 = new Person('Jack', 18)
- p1.sayName() // => Jack
- var p2 = new Person('Mike', 23)
- p2.sayName() // => Mike
解析構造函數代碼的執行
在上面的示例中,使用 new 操作符創建 Person 實例對象;
以這種方式調用構造函數會經歷以下 5 個步驟:
- 創建一個空對象,作為將要返回的對象實例。
- 將這個空對象的原型,指向構造函數的prototype屬性。先記住,后面講
- 將這個空對象賦值給函數內部的this關鍵字。
- 執行構造函數內部的代碼。
- 返回新對象 (this)
- function Person (name, age) {
- // 當使用 new 操作符調用 Person() 的時候,實際上這里會先創建一個對象
- // 然后讓內部的 this 指向新創建的對象
- // 接下來所有針對 this 的操作實際上操作的就是剛創建的這個對象
- this.name = name
- this.age = age
- this.sayName = function () {
- console.log(this.name)
- }
- // 在函數的結尾處會將 this 返回,也就是這個新對象
- }
構造函數和實例對象的關系
構造函數是根據具體的事物抽象出來的抽象模板,實例對象是根據抽象的構造函數模板得到的具體實例對象。
實例對象由構造函數而來,一個構造函數可以生成很多具體的實例對象,而每個實例對象都是獨一無二的。
每個對象都有一個 constructor 屬性,該屬性指向創建該實例的構造函數。
反推出來,每一個對象都有其構造函數
- console.log(p1.constructor === Person) // => true
- console.log(p2.constructor === Person) // => true
- console.log(p1.constructor === p2.constructor) // => true
因此,我們可以通過實例對象的 constructor 屬性判斷實例和構造函數之間的關系。
構造函數存在的問題
以構造函數為模板,創建對象,對象的屬性和方法都可以在構造函數內部定義。
- function Cat(name, color) {
- this.name = name;
- this.color = color;
- this.say = function () {
- console.log('hello'+this.name,this.color);
- };
- }
- var cat1 = new Cat('貓', '白色');
- var cat2 = new Cat('貓', '黑色');
- cat1.say();
- cat2.say();
在該示例中,從表面上看好像沒什么問題,但是實際上這樣做,有一個很大的弊端。那就是對于每一個實例對象, name 和 say 都是一模一樣的內容,每一次生成一個實例,都必須為重復的內容,多占用一些內存,如果實例對象很多,會造成極大的內存浪費。
那么,能不能將相同的內容,放到公共部分,節約計算機資源呢?
原型
JavaScript 的每個對象都會繼承一個父級對象,父級對象稱為 原型 (prototype) 對象。
原型也是一個對象,原型對象上的所有屬性和方法,都能被子對象 (派生對象) 共享,通過構造函數生成實例對象時,會自動為實例對象分配原型對象。而每一個構造函數都有一個prototype屬性,這個屬性就是實例對象的原型對象。
null 沒有自己的原型對象。
這也就意味著,我們可以把所有對象實例需要共享的屬性和方法直接定義在構造函數的 prototype 屬性上,也就是實例對象的原型對象上。
- function Cat(color) {
- this.color = color;
- }
- Cat.prototype.name = "貓";
- Cat.prototype.sayhello = function(){
- console.log('hello'+this.name,this.color);
- }
- Cat.prototype.saycolor = function (){
- console.log('hello'+this.color);
- }
- var cat1 = new Cat('白色');
- var cat2 = new Cat('黑色');
- cat1.sayhello();
- cat2.saycolor();
這時所有實例對象的 name 屬性和 sayhello() 、saycolor 方法,其實都是在同一個內存地址的對象中,也就是構造函數的 prototype 屬性上,因此就提高了運行效率節省了內存空間。
原型及原型鏈
構造函數的 prototyp 屬性,就是由這個構造函數 new 出來的所有實例對象的 原型對象
所有對象都有原型對象。
- function Cat(name, color) {
- this.name = name;
- }
- var cat1 = new Cat('貓');
- console.log(cat1.__proto__.__proto__.__proto__);
而原型對象中的屬性和方法,都可以被實例對象直接使用。
每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具有給定名字的屬性。
- 搜索首先從對象實例本身開始
- 如果在實例中找到了具有給定名字的屬性,則返回該屬性的值
- 如果沒有找到,則繼續搜索指針指向的原型對象,在原型對象中查找具有給定名字的屬性
- 如果在原型對象中找到了這個屬性,則返回該屬性的值
- 如果還是找不到,就到原型的原型去找,依次類推。
- 如果直到最頂層的Object.prototype還是找不到,則返回undefined。
而這正是多個對象實例共享原型所保存的屬性和方法的基本原理。
對象的屬性和方法,有可能是定義在自身內,也有可能是定義在它的原型對象上。由于原型本身也是對象,又有自己的原型,所以形成了一條可向上追溯的鏈條,叫 原型鏈(prototype chain)。
注意,不在要原型上形成多層鏈式查找,非常浪費資源。
內置標準庫與包裝對象
在內置標準對象中,對象是 JavaScript 語言最主要的數據類型,三種原始類型的值——數值、字符串、布爾值——在一定條件下,也會自動轉為對象,也就是原始類型的“包裝對象”(wrapper)。
所謂“包裝對象”,就是分別與數值、字符串、布爾值相對應的Number、String、Boolean三個原生對象。這三個原生對象可以把原始類型的值變成(包裝成)對象。
- var v1 = new Number(123);
- var v2 = new String('abc');
- var v3 = new Boolean(true);
- typeof v1 // "object"
- typeof v2 // "object"
- typeof v3 // "object"
- v1 === 123 // false
- v2 === 'abc' // false
- v3 === true // false
包裝對象的最大目的,首先是使得 JavaScript 的對象涵蓋所有的值,其次使得原始類型的值可以方便地調用某些方法。
原始類型的值,可以自動當作對象調用,即調用各種對象的方法和參數。
這時,JavaScript 引擎會自動將原始類型的值轉為包裝對象實例,在使用后立刻銷毀實例。
比如,字符串可以調用length屬性,返回字符串的長度。
- 'abc'.length // 3
上面代碼中,abc是一個字符串,本身不是對象,不能調用length屬性。JavaScript 引擎自動將其轉為包裝對象,在這個對象上調用length屬性。調用結束后,這個臨時對象就會被銷毀。這就叫原始類型與實例對象的自動轉換。