JS中的面向對象程序設計初識

前言:面向對象程序設計(Object-oriented programming,簡稱OOP),是一種常見的編程思想。JavaScript 的核心是支持面向對象的,同時它也提供了強大靈活的 OOP 語言能力。本文從對面向對象編程的介紹開始,和您一起探索 JavaScript 的對象模型,最后描述 JavaScript 當中面向對象編程的一些概念。(本文部分文字摘自知乎MDN阮一峰JS教程


1、什么是面向對象

  • 比較正經的回答:

把一組數據結構和處理它們的方法組成對象(object),把相同行為的對象歸納為(class),通過類的封裝(encapsulation)隱藏內部細節,通過繼承(inheritance)實現類的特化(specialization)/泛化(generalization),通過多態(polymorphism)實現基于對象類型的動態分派(dynamic dispatch)。

  • 抖機靈的回答:


  • MDN中的介紹:

面向對象編程是用抽象方式創建基于現實世界模型的一種編程模式。它使用先前建立的范例,包括模塊化,多態和封裝幾種技術。
面向對象編程可以看作是使用一系列對象相互協作的軟件設計。 在 OOP 中,每個對象能夠接收消息,處理數據和發送消息給其他對象。每個對象都可以被看作是一個擁有清晰角色或責任的獨立小機器。

  • 可嘗試理解MDN中介紹的面向對象的一些術語

Class 類
定義對象的特征。它是對象的屬性和方法的模板定義.
Object 對象
類的一個實例。
Property 屬性
對象的特征,比如顏色。
Method 方法
對象的能力,比如行走。
Constructor 構造函數
對象初始化的瞬間, 被調用的方法. 通常它的名字與包含它的類一致.
Inheritance 繼承
一個類可以繼承另一個類的特征。
Encapsulation 封裝
一種把數據和相關的方法綁定在一起使用的方法.
Abstraction 抽象
結合復雜的繼承,方法,屬性的對象能夠模擬現實的模型。
Polymorphism 多態
多意為‘許多’,態意為‘形態’。不同類可以定義相同的方法或屬性。

其實面向對象最核心的四個概念就是:抽象、繼承、封裝、多態

2、JavaScript中的面向對象編程

首先我們就要來了解一下原型編程,因為JS中實際上是弱類化的編程語言,是基于原型的而不是基于類的編程。

  • 基于原型的編程不是面向對象編程中體現的風格,且行為重用(在基于類的語言中也稱為繼承)是通過裝飾它作為原型的現有對象的過程實現的。這種模式也被稱為弱類化,原型化,或基于實例的編程。
  • 原始的(也是最典型的)基于原型語言的例子是由大衛·安格爾和蘭德爾·史密斯開發的。然而,弱類化的編程風格近來變得越來越流行,并已被諸如JavaScript,Cecil,NewtonScript,IO,MOO,REBOL,Kevo,Squeak(使用框架操縱Morphic組件),和其他幾種編程語言采用。

完整的內容可看MDN 關于JS 面向對象的介紹。本文僅做基礎的介紹。

3、命名空間

命名空間是一個容器,它允許開發人員在一個獨特的,特定于應用程序的名稱下捆綁所有的功能。 在JavaScript中,命名空間只是另一個包含方法,屬性,對象的對象。

注意:需要認識到重要的一點是:與其他面向對象編程語言不同的是,Javascript中的普通對象和命名空間在語言層面上沒有區別。比如var MySpace = {},從語言層面上,可以理解成創建了一個名為MySpace的對象,也可以理解為一個名為MySpace的命名空間,到底應該理解成哪一種主要是看你如何使用它。

創造的JavaScript命名空間背后的想法很簡單:一個全局對象被創建,所有的變量,方法和功能成為該對象的屬性。使用命名空間也最大程度地減少應用程序的名稱沖突的可能性。

舉例說明:
我們來創建一個全局變量叫做 MYAPP
var MYAPP = MYAPP || {};
在上面的代碼示例中,我們首先檢查MYAPP是否已經被定義(是否在同一文件中或在另一文件)。如果是的話,那么使用現有的MYAPP全局對象,否則,創建一個名為MYAPP的空對象用來封裝方法,函數,變量和對象。

然后我們就可以創建子命名空間:
MYAPP.event = {};

4、類

JavaScript是一種基于原型的語言,它沒類的聲明語句,比如C+ +或Java中用的。相反,JavaScript可用方法作類。定義一個類跟定義一個函數一樣簡單。在下面的例子中,我們定義了一個新類Person。

function Person() { } 
// 或
var Person = function(){ }

對象(類的實例)
我們使用 new obj 創建對象obj 的新實例, 將結果(obj 類型)賦值給一個變量方便稍后調用。舉例說明:
在下面的示例中,我們定義了一個名為Person的類,然后我們創建了兩個Person的實例(person1person2).

function Person() { }
var person1 = new Person();
var person2 = new Person();

5、構造函數

面向對象編程的第一步,就是要生成對象。前面說過,對象是單個實物的抽象。通常需要一個模板,表示某一類實物的共同特征,然后對象根據這個模板生成。

典型的面向對象編程語言(比如 C++ 和 Java),都有“類”(class)這個概念。所謂“類”就是對象的模板,對象就是“類”的實例。但是,JavaScript 語言的對象體系,不是基于“類”的,而是基于構造函數(constructor)和原型鏈(prototype)。

JavaScript 語言使用構造函數(constructor)作為對象的模板。所謂”構造函數”,就是專門用來生成實例對象的函數。它就是對象的模板,描述實例對象的基本結構。一個構造函數,可以生成多個實例對象,這些實例對象都有相同的結構。

構造函數就是一個普通的函數,但是有自己的特征和用法。如:

var Vehicle = function () {
  this.price = 1000;
};

上面代碼中,Vehicle就是構造函數。為了與普通函數區別,構造函數名字的第一個字母通常大寫。

構造函數的特點有兩個:

  • 函數體內部使用了this關鍵字,代表了所要生成的對象實例。
  • 生成對象的時候,必須使用new命令。

下面將詳細介紹一下new命令和this關鍵字。

6、new 命令

寫之前先放一個鏈接,是方大寫的關于new的文章,私以為能解決很多人關于new的疑惑。

new命令的作用,就是執行構造函數,返回一個實例對象。如:

var Vehicle = function () {
  this.price = 1000;
};
var v = new Vehicle();
v.price // 1000

上面代碼通過new命令,讓構造函數Vehicle生成一個實例對象,保存在變量v中。這個新生成的實例對象,從構造函數Vehicle得到了price屬性。new命令執行時,構造函數內部的this,就代表了新生成的實例對象,this.price表示實例對象有一個price屬性,值是1000。

var v = new Vehicle();使用new,JS默默的做了哪些事情呢:

  • 創建臨時對象
  • 將這個臨時對象賦值給函數內部的this關鍵字。
  • Viehicle.prototype = { constructor: 構造函數 }
  • 臨時對象.__proto__ = Vehicle.prototype
  • 執行 Vehicle.apply(this,arguments)
  • return thisreturn 創建的臨時對象
來自方方的示意圖

7、this 關鍵字

關于this關鍵字,我之前寫的一篇關于函數的博客初步的介紹過,本文將再次詳細的了解一下這個看起來有點坑的事物。(這里也推薦一篇方大寫的介紹this的博客 ,下面的內容也借鑒了該博客所說的思路)

  • 什么是this,最本質的概念是:this就是你 call 一個函數時,傳入的第一個參數。
  • 怎么判斷this到底指的是什么,將函數的調用形式轉換為 call 形式即可。
  • 如果是一些封裝過的函數(比如onclick()、addEventListener()等),如何判斷this
    1. 看源碼中對應的函數是怎么被 call 的(這是最靠譜的辦法)
    2. 看文檔
    3. console.log(this)
    4. 不要瞎猜,你猜不到的

舉例說明:

  • 首先來看如何將普通的函數調用轉換為call的形式:
func(p1, p2) // 等價于
func.call(undefined, p1, p2)

obj.child.method(p1, p2) // 等價于
obj.child.method.call(obj.child, p1, p2)
  • 下面看幾個例子。
    例1:
function func(){
  console.log(this)
}
func() // 轉化為下面的句子
func.call(undefined) // 可以簡寫為 func.call()

此時this即為undefined,但注意:
如果你的call傳的第一個參數是 null 或者 undefined,那么 window 對象就是默認的this(嚴格模式下默認為 undefined

  • 例2:
var obj = {
  foo: function(){
    console.log(this)
  }
}
obj.foo() // 轉化為下面的語句
obj.foo.call(obj)

本例中的this即為obj

  • 例3:
function fn (){ console.log(this) }
var arr = [fn, fn2]
arr[0]() // 這里面的 this 又是什么呢?

我們可以把 arr[0]( ) 想象為arr.0( ),雖然后者的語法錯了,但是形式與轉換代碼里的 obj.child.method(p1, p2)對應上了,于是就可以愉快的轉換了:

arr[0]() 
假想為    arr.0()
然后轉換為 arr.0.call(arr)
那么里面的 this 就是 arr 了 :)
  • 例4:(下面三個例子都是一些常見的經過封裝后的函數的this,這些函數就無法轉化成call的形式了,需要看文檔才能知道,我幫大家看了文檔,所以下面的例子就需要單獨記憶啦)
button.onclick = function(){
  console.log(this)
}
// 這里的 this 指的是 觸發事件的元素 即 button
  • 例5:
button.addEventListener('click',function(){
  console.log(this)
})
// 這里的 this 指的是 觸發事件的元素的引用 即 button
  • 例6:(jQuery)
$('ul').on('click','li',function(){
  console.log(this)
})
// 這里的 this 指的是 正在執行事件的元素 即 li
  • 例7:下面三個例子就比較繞啦
function X() {
  return obj = {
    name:'obj',
    fn1(x) {
        x.fn2()
      },
      fn2() {
        console.log(1)
        console.log(this) // A
      }
  }
}
var options = {
  name:'options',
  fn1() {},
    fn2() {
      console.log(2)
      console.log(this) // B
    }
}
var x = X()
x.fn1(options)

問:這段的代碼執行的是A還是B,打印出來的this指的是什么(不公布答案,自己思考后可在控制臺驗證結果)

  • 例8:
function X() {
  return obj = {
    name:'obj',
    fn1(x) {
        x.fn2.call(this)
      },
      fn2() {
        console.log(1)
        console.log(this) // A
      }
  }
}
var options = {
  name:'options',
  fn1() {},
    fn2() {
      console.log(2)
      console.log(this) // B
    }
}
var x = X()
x.fn1(options)

問題同上。

  • 例9:
function X() {
  return obj = {
    name: 'obj',
    options: null,
    fn1(x) {
      this.options = x
      this.fn2()
    },
    fn2() {
      console.log(1)
      this.options.fn2.call(this) // A
    }
  }
}
var options = {
  name: 'options',
  fn1() {},
  fn2() {
    console.log(2)
    console.log(this) // B
  }
}
var x = X()
x.fn1(options)

問題同上。

8、做幾個小練習吧

  1. 補全下面的代碼:
function Human(options){

} // 構造函數結束

Human.prototype.______ = ___________
Human.prototype.______ = ___________
Human.prototype.______ = ___________

var human = new Human({name:'Frank', city: 'Hangzhou'})
var human2 = new Human({name:'Jack', city: 'Hangzhou'})
  • 補全代碼,使得 human 對象滿足以下條件:
    • human 這個對象本身具有屬性 namecity
    • human.__proto__對應的對象(也就是原型)具有物種(species)、走(walk)和使用工具(useTools)這幾個屬性
    • human.__proto__.constructor === Human 為 true

human2 和 human 類似。

  • 參考答案:
function Human(options){
    this.name = options.name
    this.city = options.city

} // 構造函數結束

Human.prototype.species = 'Human'
Human.prototype.walk = function(){}
Human.prototype.useTools = function(){}

var human = new Human({name:'Frank', city: 'Hangzhou'})
var human2 = new Human({name:'Jack', city: 'Hangzhou'})
  1. 填空(本題不給答案,不確定的可以直接在控制臺測試):
var object = {}
object.__proto__ ===  ????填空1????  // 為 true

var fn = function(){}
fn.__proto__ === ????填空2????  // 為 true
fn.__proto__.__proto__ === ????填空3???? // 為 true

var array = []
array.__proto__ === ????填空4???? // 為 true
array.__proto__.__proto__ === ????填空5???? // 為 true

Function.__proto__ === ????填空6???? // 為 true
Array.__proto__ === ????填空7???? // 為 true
Object.__proto__ === ????填空8???? // 為 true

true.__proto__ === ????填空9???? // 為 true

Function.prototype.__proto__ === ????填空10???? // 為 true
  1. 在 ES5 中如何用函數模擬一個類?
  • 參考答案:
    ES 5 沒有 class 關鍵字,所以只能使用函數來模擬類。代碼如下:
    function Human(name){
      this.name = name
    }
    Human.prototype.run = function(){}
    
    var person = new Human('enoch')
    
    
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。