理解面向對象開發思想 JavaScript 是什麼 - 解析執行:輕量級解釋型的 - 語言特色:動態,頭等函數 (First-class Function) + 又稱函數是 JavaScript 中的一等公民 - 執行環境:在宿主環境(host environment)下運行,瀏覽器是最多見的 JavaScript 宿主環境 + 可是在不少非瀏覽器環境中也使用 JavaScript ,例如 node.js JavaScript 的組成 - ECMAScript - 語法規範 - 變量、數據類型、類型轉換、操做符 - 流程控制語句:判斷、循環語句 - 數組、函數、做用域、預解析 - 對象、屬性、方法、簡單類型和複雜類型的區別 - 內置對象:Math、Date、Array,基本包裝類型String、Number、Boolean - Web APIs - BOM - onload頁面加載事件,window頂級對象 - 定時器 - location、history - DOM - 獲取頁面元素,註冊事件 - 屬性操做,樣式操做 - 節點屬性,節點層級 - 動態建立元素 - 事件:註冊事件的方式、事件的三個階段、事件對象 #### JavaScript 能夠作什麼 > 阿特伍德定律: > > Any application that can be written in JavaScript, will eventually be written in JavaScript. > > 任何能夠用*JavaScript*來寫的應用,最終都將用*JavaScript*來寫 > > 阿特伍德 stackoverflow的創始人之一 - [知乎 - JavaScript 能作什麼,該作什麼?] [最流行的編程語言 JavaScript 能作什麼?] ### 瀏覽器是如何工做的 , 用戶界面,咱們所看到的瀏覽器 Browser engine 瀏覽器引擎,用來查詢和操做渲染引擎 *Rendering engine 用來顯示請求的內容,負責解析HTML、CSS,並把解析的內容顯示出來 Networking 網絡,負責發送網絡請求 *JavaScript Interpreter(解析者) JavaScript解析器,負責執行JavaScript的代碼 UI Backend UI後端,用來繪製相似組合框和彈出窗口 Data Persistence(持久化) 數據持久化,數據存儲 cookie、HTML5中的sessionStorage ``` ### JavaScript 執行過程 JavaScript 運行分爲兩個階段: - 預解析 + 全局預解析(全部變量和函數聲明都會提早;同名的函數和變量函數的優先級高) + 函數內部預解析(全部的變量、函數和形參都會參與預解析) * 函數 * 形參 * 普通變量 - 執行 先預解析全局做用域,而後執行全局做用域中的代碼, 在執行全局代碼的過程當中遇到函數調用就會先進行函數預解析,而後再執行函數內代碼。 --- ## JavaScript 面向對象編程### 面向對象介紹 #### 什麼是對象 > Everything is object (萬物皆對象) 對象究竟是什麼,咱們能夠從兩次層次來理解。 **(1) 對象是單個事物的抽象。** 一本書、一輛汽車、一我的均可以是對象,一個數據庫、一張網頁、一個與遠程服務器的鏈接也能夠是對象。當實物被抽象成對象,實物之間的關係就變成了對象之間的關係,從而就能夠模擬現實狀況,針對對象進行編程。 **(2) 對象是一個容器,封裝了屬性(property)和方法(method)。** 屬性是對象的狀態,方法是對象的行爲(完成某種任務)。好比,咱們能夠把動物抽象爲animal對象,使用「屬性」記錄具體是那一種動物,使用「方法」表示動物的某種行爲(奔跑、捕獵、休息等等)。 在實際開發中,對象是一個抽象的概念,能夠將其簡單理解爲:**數據集或功能集**。 ECMAScript-262 把對象定義爲:**無序屬性的集合,其屬性能夠包含基本值、對象或者函數**。 嚴格來說,這就至關於說對象是一組沒有特定順序的值。對象的每一個屬性或方法都有一個名字,而每一個名字都映射到一個值。 提示:每一個對象都是基於一個引用類型建立的,這些類型能夠是系統內置的原生類型,也能夠是開發人員自定義的類型。 #### 什麼是面向對象 > 面向對象不是新的東西,它只是過程式代碼的一種高度封裝,目的在於提升代碼的開發效率和可維 護性 。面向對象編程 —— Object Oriented Programming,簡稱 OOP ,是一種編程開發思想。 它將真實世界各類複雜的關係,抽象爲一個個對象,而後由對象之間的分工與合做,完成對真實世界的模擬。 在面向對象程序開發思想中,每個對象都是功能中心,具備明確分工,能夠完成接受信息、處理數據、發出信息等任務。 所以,面向對象編程具備靈活、代碼可複用、高度模塊化等特色,容易維護和開發,比起由一系列函數或指令組成的傳統的過程式編程,更適合多人合做的大型軟件項目。 面向對象與面向過程: - 面向過程就是親力親爲,事無鉅細,面面俱到,步步緊跟,有條不紊 - 面向對象就是找一個對象,指揮得結果 - 面向對象將執行者轉變成指揮者 - 面向對象不是面向過程的替代,而是面向過程的封裝 面向對象的特性: - 封裝性 - 繼承性 - [多態性]抽象 擴展閱讀: - [維基百科 - 面向對象程序設計] [知乎:如何用一句話說明什麼是面向對象思想?] [知乎:什麼是面向對象編程思想?#### 程序中面向對象的基本體現 在 JavaScript 中,全部數據類型均可以視爲對象,固然也能夠自定義對象。 自定義的對象數據類型就是面向對象中的類( Class )的概念。 javascript
咱們以一個例子來講明面向過程和麪向對象在程序流程上的不一樣之處。 假設咱們要處理學生的成績表,爲了表示一個學生的成績,面向過程的程序能夠用一個對象示: ```javascript var std1 = { name: 'Michael', score: 98 } var std2 = { name: 'Bob', score: 81 } ``` 而處理學生成績能夠經過函數實現,好比打印學生的成績: ```javascript function printScore (student) { console.log('姓名:' + student.name + ' ' + '成績:' + student.score) } ``` 若是採用面向對象的程序設計思想,咱們首選思考的不是程序的執行流程, 而是 `Student` 這種數據類型應該被視爲一個對象,這個對象擁有 `name` 和 `score` 這兩個屬性(Property)。 若是要打印一個學生的成績,首先必須建立出這個學生對應的對象,而後,給對象發一個 `printScore` 消息,讓對象本身把本身的數據打印出來。 抽象數據行爲模板(Class): ```javascript function Student(name, score) { this.name = name; this.score = score; this.printScore = function() { console.log('姓名:' + this.name + ' ' + '成績:' + this.score); } } ``` 根據模板建立具體實例對象(Instance): ```javascript var std1 = new Student('Michael', 98) var std2 = new Student('Bob', 81) ``` 實例對象具備本身的具體行爲(給對象發消息): ```javascript 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()` 建立: ```javascript var person = new Object() person.name = 'Jack' person.age = 18 person.sayName = function () { console.log(this.name) } ``` 每次建立經過 `new Object()` 比較麻煩,因此能夠經過它的簡寫形式對象字面量來建立: ```javascript var person = { name: 'Jack', age: 18, sayName: function () { console.log(this.name) } } ``` 對於上面的寫法當然沒有問題,可是假如咱們要生成兩個 `person` 實例對象呢? ```javascript var person1 = { name: 'Jack', age: 18, sayName: function () { console.log(this.name) } } var person2 = { name: 'Mike', age: 16, sayName: function () { console.log(this.name) } } ``` 經過上面的代碼咱們不難看出,這樣寫的代碼太過冗餘,重複性過高。 #### 簡單方式的改進:工廠函數 咱們能夠寫一個函數,解決代碼重複問題: ```javascript function createPerson (name, age) { return { name: name, age: age, sayName: function () { console.log(this.name) } } } ``` 而後生成實例對象: ```javascript var p1 = createPerson('Jack', 18) var p2 = createPerson('Mike', 18) ``` 這樣封裝確實爽多了,經過工廠模式咱們解決了建立多個類似對象代碼冗餘的問題, 但卻沒有解決對象識別的問題(即怎樣知道一個對象的類型)工廠函數沒法經過 typeof 判斷對象的數據類型 經過實例判斷,例如:arr instanceof Array。 ### 構造函數 內容引導: - 構造函數語法 - 分析構造函數 - 構造函數和實例對象的關係 + 實例的 constructor 屬性 + instanceof 操做符 - 普通函數調用和構造函數調用的區別 - 構造函數的返回值 - 構造函數的問題 #### 更優雅的工廠函數:構造函數 一種更優雅的工廠函數就是下面這樣,構造函數: ```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 ``` #### 解析構造函數代碼的執行 在上面的示例中,`Person()` 函數取代了 `createPerson()` 函數,可是實現效果是同樣的。 這是爲何呢? 咱們注意到,`Person()` 中的代碼與 `createPerson()` 有如下幾點不一樣之處: - 沒有顯示的建立對象 - 直接將屬性和方法賦給了 `this` 對象 - 沒有 `return` 語句 - 函數名使用的是大寫的 `Person` 而要建立 `Person` 實例,則必須使用 `new` 操做符。 以這種方式調用構造函數會經歷如下 4 個步驟: 1. 建立一個新對象 2. 將構造函數的做用域賦給新對象(所以 this 就指向了這個新對象) 3. 執行構造函數中的代碼 4. 返回新對象 下面是具體的僞代碼: ```javascript 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 } ``` #### 構造函數和實例對象的關係 使用構造函數的好處不只僅在於代碼的簡潔性,更重要的是咱們能夠識別對象的具體類型了。 在每個實例對象中同時有一個 `constructor` 屬性,該屬性指向建立該實例的構造函數: ```javascript console.log(p1.constructor === Person) // => true console.log(p2.constructor === Person) // => true console.log(p1.constructor === p2.constructor) // => true ``` 對象的 `constructor` 屬性最初是用來標識對象類型的, 可是,若是要檢測對象的類型,仍是使用 `instanceof` 操做符更可靠一些: ```javascript console.log(p1 instanceof Person) // => true console.log(p2 instanceof Person) // => true ``` 總結: - 構造函數是根據具體的事物抽象出來的抽象模板 - 實例對象是根據抽象的構造函數模板獲得的具體實例對象 - 每個實例對象都具備一個 `constructor` 屬性,指向建立該實例的構造函數 + 注意: `constructor` 是實例的屬性的說法不嚴謹,具體後面的原型會講到 - 能夠經過實例的 `constructor` 屬性判斷實例和構造函數之間的關係 + 注意:這種方式不嚴謹,推薦使用 `instanceof` 操做符,後面學原型會解釋爲何 #### 構造函數的問題 使用構造函數帶來的最大的好處就是建立對象更方便了,可是其自己也存在一個浪費內存的題: ```javascript 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('Tom', 18) var p2 = new Person('Jack', 16) ``` 在該示例中,從表面上好像沒什麼問題,可是實際上這樣作,有一個很大的弊端。 那就是對於每個實例對象,`type` 和 `sayHello` 都是如出一轍的內容, 每一次生成一個實例,都必須爲重複的內容,多佔用一些內存,若是實例對象不少,會形成極大的內存浪費。 ```javascript console.log(p1.sayHello === p2.sayHello) // => false ``` 對於這種問題咱們能夠把須要共享的函數定義到構造函數外部: ```javascript 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('Top', 18) var p2 = new Person('Jack', 16) console.log(p1.sayHello === p2.sayHello) // => true ``` 這樣確實能夠了,可是若是有多個須要共享的函數的話就會形成全局命名空間衝突的問題。 你確定想到了能夠把多個函數放到一個對象中用來避免全局命名空間衝突的問題: ```javascript 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` 對象上。 ```javascript 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` 屬性,該屬性是一個對象。 ```javascript function F () {} console.log(F.prototype) // => object F.prototype.sayHi = function () { console.log('hi!') } ``` 構造函數的 `prototype` 對象默認都有一個 `constructor` 屬性,指向 `prototype` 對象所在函數。 ```javascript console.log(F.prototype.constructor === F) // => true ``` 經過構造函數獲得的實例對象內部會包含一個指向構造函數的 `prototype` 對象的指針 `__proto__`。 ```javascript var instance = new F() console.log(instance.__proto__ === F.prototype) // => true ``` java
`__proto__` 是非標準屬性。 node
實例對象能夠直接訪問原型對象成員。 ```javascript 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` 。 爲減小沒必要要的輸入,更常見的作法是用一個包含全部屬性和方法的對象字面量來重寫整個原型對象: ```javascript 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` 的指向正確,建議的寫法是: ```javascript 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 屬性對象。 編程