Web前端學習筆記——JavaScript之面向物件程式設計
JavaScript 高階
基本概念複習
由於 JavaScript 高階還是針對 JavaScript 語言本身的一個進階學習,所以在開始之前我們先對以前所學過的 JavaScript 相關知識點做一個快速複習總結。
重新介紹 JavaScript
JavaScript 是什麼
- 解析執行:輕量級解釋型的,或是 JIT 編譯型的程式設計語言
- 語言特點:動態,頭等函式 (First-class Function)
- 又稱函式是 JavaScript 中的一等公民
- 執行環境:在宿主環境(host environment)下執行,瀏覽器是最常見的 JavaScript 宿主環境
- 但是在很多非瀏覽器環境中也使用 JavaScript ,例如 node.js
- 程式設計正規化:基於原型、多正規化的動態指令碼語言,並且支援面向物件、命令式和宣告式(如:函數語言程式設計)程式設計風格
JavaScript 與瀏覽器的關係
JavaScript 的組成
組成部分 | 說明 |
---|---|
Ecmascript | 描述了該語言的語法和基本物件 |
DOM | 描述了處理網頁內容的方法和介面 |
BOM | 描述了與瀏覽器進行互動的方法和介面 |
JavaScript 可以做什麼
Any application that can be written in JavaScript, will eventually be written in JavaScript. 凡是能用 JavaScript 寫出來的,最終都會用 JavaScript 寫出來
JavaScript 發展歷史
- JavaScript 的誕生
- JavaScript 與 Ecmascript 的關係
- JavaScript 與 Java 的關係
- JavaScript 的版本
- JavaScript 周邊大事記
小結
基本概念
本小節快速過即可,主要是對學過的內容做知識點梳理。
- 語法
- 區分大小寫
- 識別符號
- 註釋
- 嚴格模式
- 語句
- 關鍵字和保留字
- 變數
- 資料型別
- typeof 操作符
- Undefined
- Null
- Boolean
- Number
- String
- Object
- 操作符
- 流程控制語句
- 函式
JavaScript 中的資料型別
JavaScript 有 5 種簡單資料型別:Undefined、Null、Boolean、Number、String
Object
。
基本型別(值型別)
- Undefined
- Null
- Boolean
- Number
- String
複雜型別(引用型別)
- Object
- Array
- Date
- RegExp
- Function
- 基本包裝型別
- Boolean
- Number
- String
- 單體內建物件
- Global
- Math
型別檢測
typeof
instanceof
Object.prototype.toString.call()
值型別和引用型別在記憶體中的儲存方式(畫圖說明)
- 值型別按值儲存
- 引用型別按引用儲存
值型別複製和引用型別複製(畫圖說明)
- 值型別按值複製
- 引用型別按引用複製
值型別和引用型別引數傳遞(畫圖說明)
- 值型別按值傳遞
- 引用型別按引用傳遞
值型別與引用型別的差別
- 基本型別在記憶體中佔據固定大小的空間,因此被儲存在棧記憶體中
- 從一個變數向另一個變數複製基本型別的值,複製的是值的副本
- 引用型別的值是物件,儲存在堆記憶體
- 包含引用型別值的變數實際上包含的並不是物件本身,而是一個指向該物件的指標
- 從一個變數向另一個變數複製引用型別的值的時候,複製是引用指標,因此兩個變數最終都指向同一個物件
小結
- 型別檢測方式
- 值型別和引用型別的儲存方式
- 值型別複製和引用型別複製
- 方法引數中 值型別資料傳遞 和 引用型別資料傳遞
JavaScript 執行過程
JavaScript 執行分為兩個階段:
- 預解析
- 全域性預解析(所有變數和函式宣告都會提前;同名的函式和變數函式的優先順序高)
- 函式內部預解析(所有的變數、函式和形參都會參與預解析)
- 函式
- 形參
- 普通變數
- 執行
先預解析全域性作用域,然後執行全域性作用域中的程式碼, 在執行全域性程式碼的過程中遇到函式呼叫就會先進行函式預解析,然後再執行函式內程式碼。
JavaScript 面向物件程式設計
面向物件介紹
什麼是物件
Everything is object (萬物皆物件)
物件到底是什麼,我們可以從兩次層次來理解。
(1) 物件是單個事物的抽象。
一本書、一輛汽車、一個人都可以是物件,一個數據庫、一張網頁、一個與遠端伺服器的連線也可以是物件。當實物被抽象成物件,實物之間的關係就變成了物件之間的關係,從而就可以模擬現實情況,針對物件進行程式設計。
(2) 物件是一個容器,封裝了屬性(property)和方法(method)。
屬性是物件的狀態,方法是物件的行為(完成某種任務)。比如,我們可以把動物抽象為animal物件,使用“屬性”記錄具體是那一種動物,使用“方法”表示動物的某種行為(奔跑、捕獵、休息等等)。
在實際開發中,物件是一個抽象的概念,可以將其簡單理解為:資料集或功能集。
ECMAScript-262 把物件定義為:無序屬性的集合,其屬性可以包含基本值、物件或者函式。 嚴格來講,這就相當於說物件是一組沒有特定順序的值。物件的每個屬性或方法都有一個名字,而每個名字都 對映到一個值。
提示:每個物件都是基於一個引用型別建立的,這些型別可以是系統內建的原生型別,也可以是開發人員自定義的型別。
什麼是面向物件
面向物件不是新的東西,它只是過程式程式碼的一種高度封裝,目的在於提高程式碼的開發效率和可維護性。
面向物件程式設計 —— Object Oriented Programming,簡稱 OOP ,是一種程式設計開發思想。 它將真實世界各種複雜的關係,抽象為一個個物件,然後由物件之間的分工與合作,完成對真實世界的模擬。
在面向物件程式開發思想中,每一個物件都是功能中心,具有明確分工,可以完成接受資訊、處理資料、發出資訊等任務。 因此,面向物件程式設計具有靈活、程式碼可複用、高度模組化等特點,容易維護和開發,比起由一系列函式或指令組成的傳統的程序式程式設計(procedural programming),更適合多人合作的大型軟體專案。
面向物件與面向過程:
- 面向過程就是親力親為,事無鉅細,面面俱到,步步緊跟,有條不紊
- 面向物件就是找一個物件,指揮得結果
- 面向物件將執行者轉變成指揮者
- 面向物件不是面向過程的替代,而是面向過程的封裝
面向物件的特性:
- 封裝性
- 繼承性
- [多型性]
擴充套件閱讀:
程式中面向物件的基本體現
在 JavaScript 中,所有資料型別都可以視為物件,當然也可以自定義物件。 自定義的物件資料型別就是面向物件中的類( Class )的概念。
我們以一個例子來說明面向過程和麵向物件在程式流程上的不同之處。
假設我們要處理學生的成績表,為了表示一個學生的成績,面向過程的程式可以用一個物件表示:
var std1 = { name: 'Michael', score: 98 }
var std2 = { name: 'Bob', score: 81 }
而處理學生成績可以通過函式實現,比如列印學生的成績:
function printScore (student) {
console.log('姓名:' + student.name + ' ' + '成績:' + student.score)
}
如果採用面向物件的程式設計思想,我們首選思考的不是程式的執行流程,
而是 Student
這種資料型別應該被視為一個物件,這個物件擁有 name
和 score
這兩個屬性(Property)。
如果要列印一個學生的成績,首先必須創建出這個學生對應的物件,然後,給物件發一個 printScore
訊息,讓物件自己把自己的資料打印出來。
抽象資料行為模板(Class):
function Student (name, score) {
this.name = name
this.score = score
}
Student.prototype.printScore = function () {
console.log('姓名:' + this.name + ' ' + '成績:' + this.score)
}
根據模板建立具體例項物件(Instance):
var std1 = new Student('Michael', 98)
var std2 = new Student('Bob', 81)
例項物件具有自己的具體行為(給物件發訊息):
std1.printScore() // => 姓名:Michael 成績:98
std2.printScore() // => 姓名:Bob 成績 81
面向物件的設計思想是從自然界中來的,因為在自然界中,類(Class)和例項(Instance)的概念是很自然的。 Class 是一種抽象概念,比如我們定義的 Class——Student ,是指學生這個概念, 而例項(Instance)則是一個個具體的 Student ,比如, Michael 和 Bob 是兩個具體的 Student 。
所以,面向物件的設計思想是:
- 抽象出 Class
- 根據 Class 建立 Instance
- 指揮 Instance 得結果
面向物件的抽象程度又比函式要高,因為一個 Class 既包含資料,又包含操作資料的方法。
建立物件
簡單方式
我們可以直接通過 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)
}
}
對於上面的寫法固然沒有問題,但是假如我們要生成兩個 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)
}
}
通過上面的程式碼我們不難看出,這樣寫的程式碼太過冗餘,重複性太高。
簡單方式的改進:工廠函式
我們可以寫一個函式,解決程式碼重複問題:
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)
這樣封裝確實爽多了,通過工廠模式我們解決了建立多個相似物件程式碼冗餘的問題, 但卻沒有解決物件識別的問題(即怎樣知道一個物件的型別)。
建構函式
內容引導:
- 建構函式語法
- 分析建構函式
- 建構函式和例項物件的關係
- 例項的 constructor 屬性
- instanceof 操作符
- 普通函式呼叫和建構函式呼叫的區別
- 建構函式的返回值
- 建構函式的靜態成員和例項成員
- 函式也是物件
- 例項成員
- 靜態成員
- 建構函式的問題
更優雅的工廠函式:建構函式
一種更優雅的工廠函式就是下面這樣,建構函式:
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()
函式取代了 createPerson()
函式,但是實現效果是一樣的。
這是為什麼呢?
我們注意到,Person()
中的程式碼與 createPerson()
有以下幾點不同之處:
- 沒有顯示的建立物件
- 直接將屬性和方法賦給了
this
物件 - 沒有
return
語句 - 函式名使用的是大寫的
Person
而要建立 Person
例項,則必須使用 new
操作符。
以這種方式呼叫建構函式會經歷以下 4 個步驟:
- 建立一個新物件
- 將建構函式的作用域賦給新物件(因此 this 就指向了這個新物件)
- 執行建構函式中的程式碼
- 返回新物件
下面是具體的虛擬碼:
function Person (name, age) {
// 當使用 new 操作符呼叫 Person() 的時候,實際上這裡會先建立一個物件
// var instance = {}
// 然後讓內部的 this 指向 instance 物件
// this = instance
// 接下來所有針對 this 的操作實際上操作的就是 instance
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
// 在函式的結尾處會將 this 返回,也就是 instance
// return this
}
建構函式和例項物件的關係
使用建構函式的好處不僅僅在於程式碼的簡潔性,更重要的是我們可以識別物件的具體型別了。
在每一個例項物件中的__proto__中同時有一個 constructor
屬性,該屬性指向建立該例項的建構函式:
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
總結:
- 建構函式是根據具體的事物抽象出來的抽象模板
- 例項物件是根據抽象的建構函式模板得到的具體例項物件
- 每一個例項物件都具有一個
constructor
屬性,指向建立該例項的建構函式- 注意:
constructor
是例項的屬性的說法不嚴謹,具體後面的原型會講到
- 注意:
- 可以通過例項的
constructor
屬性判斷例項和建構函式之間的關係- 注意:這種方式不嚴謹,推薦使用
instanceof
操作符,後面學原型會解釋為什麼
- 注意:這種方式不嚴謹,推薦使用
建構函式的問題
使用建構函式帶來的最大的好處就是建立物件更方便了,但是其本身也存在一個浪費記憶體的問題:
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
都是一模一樣的內容,
每一次生成一個例項,都必須為重複的內容,多佔用一些記憶體,如果例項物件很多,會造成極大的記憶體浪費。
console.log(p1.sayHello === p2.sayHello) // => false
對於這種問題我們可以把需要共享的函式定義到建構函式外部:
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
這樣確實可以了,但是如果有多個需要共享的函式的話就會造成全域性名稱空間衝突的問題。
你肯定想到了可以把多個函式放到一個物件中用來避免全域性名稱空間衝突的問題:
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
至此,我們利用自己的方式基本上解決了建構函式的記憶體浪費問題。 但是程式碼看起來還是那麼的格格不入,那有沒有更好的方式呢?
小結
- 建構函式語法
- 分析建構函式
- 建構函式和例項物件的關係
- 例項的 constructor 屬性
- instanceof 操作符
- 建構函式的問題
原型
內容引導:
- 使用 prototype 原型物件解決建構函式的問題
- 分析 建構函式、prototype 原型物件、例項物件 三者之間的關係
- 屬性成員搜尋原則:原型鏈
- 例項物件讀寫原型物件中的成員
- 原型物件的簡寫形式
- 原生物件的原型
- Object
- Array
- String
- …
- 原型物件的問題
- 構造的函式和原型物件使用建議
更好的解決方案: prototype
Javascript 規定,每一個建構函式都有一個 prototype
屬性,指向另一個物件。
這個物件的所有屬性和方法,都會被建構函式的例項繼承。
這也就意味著,我們可以把所有物件例項需要共享的屬性和方法直接定義在 prototype
物件上。
function Person (name, age) {
this.name = name
this.age = age
}
console.log(Person.prototype)
Person.prototype.type = 'human'
Person.prototype.sayName = function () {
console.log(this.name)
}
var p1 = new Person(...)
var p2 = new Person(...)
console.log(p1.sayName === p2.sayName) // => true
這時所有例項的 type
屬性和 sayName()
方法,
其實都是同一個記憶體地址,指向 prototype
物件,因此就提高了執行效率。
建構函式、例項、原型三者之間的關係
任何函式都具有一個 prototype
屬性,該屬性是一個物件。
function F () {}
console.log(F.prototype) // => object
F.prototype.sayHi = function () {
console.log('hi!')
}
建構函式的 prototype
物件預設都有一個 constructor
屬性,指向 prototype
物件所在函式。
console.log(F.constructor === F) // => true
通過建構函式得到的例項物件內部會包含一個指向建構函式的 prototype
物件的指標 __proto__
。
var instance = new F()
console.log(instance.__proto__ === F.prototype) // => true
`__proto__` 是非標準屬性。
例項物件可以直接訪問原型物件成員。
instance.sayHi() // => hi!
總結:
- 任何函式都具有一個
prototype
屬性,該屬性是一個物件 - 建構函式的
prototype
物件預設都有一個constructor
屬性,指向prototype
物件所在函式 - 通過建構函式得到的例項物件內部會包含一個指向建構函式的
prototype
物件的指標__proto__
- 所有例項都直接或間接繼承了原型物件的成員
屬性成員的搜尋原則:原型鏈
瞭解了 建構函式-例項-原型物件 三者之間的關係後,接下來我們來解釋一下為什麼例項物件可以訪問原型物件中的成員。
每當程式碼讀取某個物件的某個屬性時,都會執行一次搜尋,目標是具有給定名字的屬性
- 搜尋首先從物件例項本身開始
- 如果在例項中找到了具有給定名字的屬性,則返回該屬性的值
- 如果沒有找到,則繼續搜尋指標指向的原型物件,在原型物件中查詢具有給定名字的屬性
- 如果在原型物件中找到了這個屬性,則返回該屬性的值
也就是說,在我們呼叫 person1.sayName()
的時候,會先後執行兩次搜尋:
- 首先,解析器會問:“例項 person1 有 sayName 屬性嗎?”答:“沒有。
- ”然後,它繼續搜尋,再問:“ person1 的原型有 sayName 屬性嗎?”答:“有。
- ”於是,它就讀取那個儲存在原型物件中的函式。
- 當我們呼叫 person2.sayName() 時,將會重現相同的搜尋過程,得到相同的結果。
而這正是多個物件例項共享原型所儲存的屬性和方法的基本原理。
總結:
- 先在自己身上找,找到即返回
- 自己身上找不到,則沿著原型鏈向上查詢,找到即返回
- 如果一直到原型鏈的末端還沒有找到,則返回
undefined
例項物件讀寫原型物件成員
讀取:
- 先在自己身上找,找到即返回
- 自己身上找不到,則沿著原型鏈向上查詢,找到即返回
- 如果一直到原型鏈的末端還沒有找到,則返回
undefined
值型別成員寫入(例項物件.值型別成員 = xx
):
- 當例項期望重寫原型物件中的某個普通資料成員時實際上會把該成員新增到自己身上
- 也就是說該行為實際上會遮蔽掉對原型物件成員的訪問
引用型別成員寫入(例項物件.引用型別成員 = xx
):
- 同上
複雜型別修改(例項物件.成員.xx = xx
):
- 同樣會先在自己身上找該成員,如果自己身上找到則直接修改
- 如果自己身上找不到,則沿著原型鏈繼續查詢,如果找到則修改
- 如果一直到原型鏈的末端還沒有找到該成員,則報錯(
例項物件.undefined.xx = xx
)
更簡單的原型語法
我們注意到,前面例子中每新增一個屬性和方法就要敲一遍 Person.prototype
。
為減少不必要的輸入,更常見的做法是用一個包含所有屬性和方法的物件字面量來重寫整個原型物件:
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + ',我今年' + this.age + '歲了')
}
}
在該示例中,我們將 Person.prototype
重置到了一個新的物件。
這樣做的好處就是為 Person.prototype
新增成員簡單了,但是也會帶來一個問題,那就是原型物件丟失了 constructor
成員。
所以,我們為了保持 constructor
的指向正確,建議的寫法是:
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
constructor: Person, // => 手動將 constructor 指向正確的建構函式
type: 'human',
sayHello: function () {
console.log('我叫' + this.name + ',我今年' + this.age + '歲了')
}
}
原生物件的原型
所有函式都有 prototype 屬性物件。
- Object.prototype
- Function.prototype
- Array.prototype
- String.prototype
- Number.prototype
- Date.prototype
- …
練習:為陣列物件和字串物件擴充套件原型方法。
原型物件的問題
- 共享陣列
- 共享物件
如果真的希望可以被例項物件之間共享和修改這些共享資料那就不是問題。但是如果不希望例項之間共享和修改這些共享資料則就是問題。
一個更好的建議是,最好不要讓例項之間互相共享這些陣列或者物件成員,一旦修改的話會導致資料的走向很不明確而且難以維護。
原型物件使用建議
- 私有成員(一般就是非函式成員)放到建構函式中
- 共享成員(一般就是函式)放到原型物件中
- 如果重置了
prototype
記得修正constructor
的指向
案例:隨機方塊
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>title</title>
<style>
.map{
width: 800px;
height: 600px;
background-color: #CCC;
position: relative;
}
</style>
</head>
<body>
<div class="map"></div>
<script src="common.js"></script>
<script>
//產生隨機數物件的
(function (window) {
function Random() {
}
Random.prototype.getRandom=function (min,max) {
return Math.floor(Math.random()*(max-min)+min);
};
//把區域性物件暴露給window頂級物件,就成了全域性的物件
window.Random=new Random();
})(window);//自呼叫建構函式的方式,分號一定要加上
//產生小方塊物件
(function (window)
相關推薦
no