JavaScript 運行分為兩個階段:
先預(yù)解析全局作用域,然后執(zhí)行全局作用域中的代碼,
在執(zhí)行全局代碼的過程中遇到函數(shù)調(diào)用就會先進行函數(shù)預(yù)解析,然后再執(zhí)行函數(shù)內(nèi)代碼。
Everything is object (萬物皆對象)
對象到底是什么,我們可以從兩次層次來理解。
(1) 對象是單個事物的抽象。
一本書、一輛汽車、一個人都可以是對象,一個數(shù)據(jù)庫、一張網(wǎng)頁、一個與遠程服務(wù)器的連接也可以是對象。當實物被抽象成對象,實物之間的關(guān)系就變成了對象之間的關(guān)系,從而就可以模擬現(xiàn)實情況,針對對象進行編程。
(2) 對象是一個容器,封裝了屬性(property)和方法(method)。
屬性是對象的狀態(tài),方法是對象的行為(完成某種任務(wù))。比如,我們可以把動物抽象為animal對象,使用“屬性”記錄具體是那一種動物,使用“方法”表示動物的某種行為(奔跑、捕獵、休息等等)。
在實際開發(fā)中,對象是一個抽象的概念,可以將其簡單理解為:數(shù)據(jù)集或功能集。
ECMAScript-262 把對象定義為:無序?qū)傩缘募?#xff0c;其屬性可以包含基本值、對象或者函數(shù)。
嚴格來講,這就相當于說對象是一組沒有特定順序的值。對象的每個屬性或方法都有一個名字,而每個名字都
映射到一個值。
提示:每個對象都是基于一個引用類型創(chuàng)建的,這些類型可以是系統(tǒng)內(nèi)置的原生類型,也可以是開發(fā)人員自定義的類型。
面向?qū)ο蟛皇切碌臇|西,它只是過程式代碼的一種高度封裝,目的在于提高代碼的開發(fā)效率和可維護性。
面向?qū)ο缶幊?—— Object Oriented Programming,簡稱 OOP ,是一種編程開發(fā)思想。
它將真實世界各種復雜的關(guān)系,抽象為一個個對象,然后由對象之間的分工與合作,完成對真實世界的模擬。
在面向?qū)ο蟪绦蜷_發(fā)思想中,每一個對象都是功能中心,具有明確分工,可以完成接受信息、處理數(shù)據(jù)、發(fā)出信息等任務(wù)。
因此,面向?qū)ο缶幊叹哂?mark>靈活、代碼可復用、高度模塊化等特點,容易維護和開發(fā),比起由一系列函數(shù)或指令組成的傳統(tǒng)的過程式編程(procedural programming),更適合多人合作的大型軟件項目。
面向?qū)ο笈c面向過程:
面向?qū)ο蟮奶匦?#xff1a;
擴展閱讀:
在 JavaScript 中,所有數(shù)據(jù)類型都可以視為對象,當然也可以自定義對象。
自定義的對象數(shù)據(jù)類型就是面向?qū)ο笾械念?#xff08; Class )的概念。
我們以一個例子來說明面向過程和面向?qū)ο笤诔绦蛄鞒躺系牟煌帯?/p>
假設(shè)我們要處理學生的成績表,為了表示一個學生的成績,面向過程的程序可以用一個對象表示:
var std1 = { name: 'Michael', score: 98 }
var std2 = { name: 'Bob', score: 81 }
而處理學生成績可以通過函數(shù)實現(xiàn),比如打印學生的成績:
function printScore (student) {
console.log('姓名:' + student.name + ' ' + '成績:' + student.score)
}
如果采用面向?qū)ο蟮某绦蛟O(shè)計思想,我們首選思考的不是程序的執(zhí)行流程,
而是 Student
這種數(shù)據(jù)類型應(yīng)該被視為一個對象,這個對象擁有 name
和 score
這兩個屬性(Property)。
如果要打印一個學生的成績,首先必須創(chuàng)建出這個學生對應(yīng)的對象,然后,給對象發(fā)一個 printScore
消息,讓對象自己把自己的數(shù)據(jù)打印出來。
抽象數(shù)據(jù)行為模板(Class):
function Student (name, score) {
this.name = name
this.score = score
}
Student.prototype.printScore = function () {
console.log('姓名:' + this.name + ' ' + '成績:' + this.score)
}
根據(jù)模板創(chuàng)建具體實例對象(Instance):
var std1 = new Student('Michael', 98)
var std2 = new Student('Bob', 81)
實例對象具有自己的具體行為(給對象發(fā)消息):
std1.printScore() // => 姓名:Michael 成績:98
std2.printScore() // => 姓名:Bob 成績 81
面向?qū)ο蟮脑O(shè)計思想是從自然界中來的,因為在自然界中,類(Class)和實例(Instance)的概念是很自然的。
Class 是一種抽象概念,比如我們定義的 Class——Student ,是指學生這個概念,
而實例(Instance)則是一個個具體的 Student ,比如, Michael 和 Bob 是兩個具體的 Student 。
所以,面向?qū)ο蟮脑O(shè)計思想是:
面向?qū)ο蟮某橄蟪潭扔直群瘮?shù)要高,因為一個 Class 既包含數(shù)據(jù),又包含操作數(shù)據(jù)的方法。
我們可以直接通過 new Object()
創(chuàng)建:
var person = new Object()
person.name = 'Jack'
person.age = 18
person.sayName = function () {
console.log(this.name)
}
每次創(chuàng)建通過 new Object()
比較麻煩,所以可以通過它的簡寫形式對象字面量來創(chuàng)建:
var person = {
name: 'Jack',
age: 18,
sayName: function () {
console.log(this.name)
}
}
對于上面的寫法固然沒有問題,但是假如我們要生成兩個 person
實例對象呢?
var person1 = {
name: 'Jack',
age: 18,
sayName: function () {
console.log(this.name)
}
}
var person2 = {
name: 'Mike',
age: 16,
sayName: function () {
console.log(this.name)
}
}
通過上面的代碼我們不難看出,這樣寫的代碼太過冗余,重復性太高。
我們可以寫一個函數(shù),解決代碼重復問題:
function createPerson (name, age) {
return {
name: name,
age: age,
sayName: function () {
console.log(this.name)
}
}
}
然后生成實例對象:
var p1 = createPerson('Jack', 18)
var p2 = createPerson('Mike', 18)
這樣封裝確實爽多了,通過工廠模式我們解決了創(chuàng)建多個相似對象代碼冗余的問題,
但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)。
一種更優(yōu)雅的工廠函數(shù)就是下面這樣,構(gòu)造函數(shù):
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
在上面的示例中,Person()
函數(shù)取代了 createPerson()
函數(shù),但是實現(xiàn)效果是一樣的。
這是為什么呢?
我們注意到,Person()
中的代碼與 createPerson()
有以下幾點不同之處:
this
對象return
語句Person
而要創(chuàng)建 Person
實例,則必須使用 new
操作符。
以這種方式調(diào)用構(gòu)造函數(shù)會經(jīng)歷以下 4 個步驟:
下面是具體的偽代碼:
function Person (name, age) {
// 當使用 new 操作符調(diào)用 Person() 的時候,實際上這里會先創(chuàng)建一個對象
// var instance = {}
// 然后讓內(nèi)部的 this 指向 instance 對象
// this = instance
// 接下來所有針對 this 的操作實際上操作的就是 instance
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
// 在函數(shù)的結(jié)尾處會將 this 返回,也就是 instance
// return this
}
使用構(gòu)造函數(shù)的好處不僅僅在于代碼的簡潔性,更重要的是我們可以識別對象的具體類型了。
在每一個實例對象中的__proto__中同時有一個 constructor
屬性,該屬性指向創(chuàng)建該實例的構(gòu)造函數(shù):
console.log(p1.constructor === Person) // => true
console.log(p2.constructor === Person) // => true
console.log(p1.constructor === p2.constructor) // => true
對象的 constructor
屬性最初是用來標識對象類型的,
但是,如果要檢測對象的類型,還是使用 instanceof
操作符更可靠一些:
console.log(p1 instanceof Person) // => true
console.log(p2 instanceof Person) // => true
總結(jié):
constructor
屬性,指向創(chuàng)建該實例的構(gòu)造函數(shù)
constructor
是實例的屬性的說法不嚴謹,具體后面的原型會講到constructor
屬性判斷實例和構(gòu)造函數(shù)之間的關(guān)系
instanceof
操作符,后面學原型會解釋為什么使用構(gòu)造函數(shù)帶來的最大的好處就是創(chuàng)建對象更方便了,但是其本身也存在一個浪費內(nèi)存的問題:
function Person (name, age) {
this.name = name
this.age = age
this.type = 'human'
this.sayHello = function () {
console.log('hello ' + this.name)
}
}
var p1 = new Person('lpz', 18)
var p2 = new Person('Jack', 16)
在該示例中,從表面上好像沒什么問題,但是實際上這樣做,有一個很大的弊端。
那就是對于每一個實例對象,type
和 sayHello
都是一模一樣的內(nèi)容,
每一次生成一個實例,都必須為重復的內(nèi)容,多占用一些內(nèi)存,如果實例對象很多,會造成極大的內(nèi)存浪費。
console.log(p1.sayHello === p2.sayHello) // => false
對于這種問題我們可以把需要共享的函數(shù)定義到構(gòu)造函數(shù)外部:
function sayHello = function () {
console.log('hello ' + this.name)
}
function Person (name, age) {
this.name = name
this.age = age
this.type = 'human'
this.sayHello = sayHello
}
var p1 = new Person('lpz', 18)
var p2 = new Person('Jack', 16)
console.log(p1.sayHello === p2.sayHello) // => true
這樣確實可以了,但是如果有多個需要共享的函數(shù)的話就會造成全局命名空間沖突的問題。
你肯定想到了可以把多個函數(shù)放到一個對象中用來避免全局命名空間沖突的問題:
var fns = {
sayHello: function () {
console.log('hello ' + this.name)
},
sayAge: function () {
console.log(this.age)
}
}
function Person (name, age) {
this.name = name
this.age = age
this.type = 'human'
this.sayHello = fns.sayHello
this.sayAge = fns.sayAge
}
var p1 = new Person('lpz', 18)
var p2 = new Person('Jack', 16)
console.log(p1.sayHello === p2.sayHello) // => true
console.log(p1.sayAge === p2.sayAge) // => true
至此,我們利用自己的方式基本上解決了構(gòu)造函數(shù)的內(nèi)存浪費問題。
但是代碼看起來還是那么的格格不入,那有沒有更好的方式呢?
那就要通過原型對象的方法來建立,下面這篇博文進行介紹