JS中的面向?qū)ο蟪绦蛟O(shè)計(jì)初識(shí)

前言:面向?qū)ο蟪绦蛟O(shè)計(jì)(Object-oriented programming,簡(jiǎn)稱OOP),是一種常見的編程思想。JavaScript 的核心是支持面向?qū)ο蟮?,同時(shí)它也提供了強(qiáng)大靈活的 OOP 語(yǔ)言能力。本文從對(duì)面向?qū)ο缶幊痰慕榻B開始,和您一起探索 JavaScript 的對(duì)象模型,最后描述 JavaScript 當(dāng)中面向?qū)ο缶幊痰囊恍└拍?。(本文部分文字摘?a target="_blank" rel="nofollow">知乎、MDN、阮一峰JS教程


1、什么是面向?qū)ο?/h2>
  • 比較正經(jīng)的回答:

把一組數(shù)據(jù)結(jié)構(gòu)和處理它們的方法組成對(duì)象(object),把相同行為的對(duì)象歸納為(class),通過類的封裝(encapsulation)隱藏內(nèi)部細(xì)節(jié),通過繼承(inheritance)實(shí)現(xiàn)類的特化(specialization)/泛化(generalization),通過多態(tài)(polymorphism)實(shí)現(xiàn)基于對(duì)象類型的動(dòng)態(tài)分派(dynamic dispatch)。

  • 抖機(jī)靈的回答:


  • MDN中的介紹:

面向?qū)ο缶幊淌怯贸橄蠓绞絼?chuàng)建基于現(xiàn)實(shí)世界模型的一種編程模式。它使用先前建立的范例,包括模塊化,多態(tài)和封裝幾種技術(shù)。
面向?qū)ο缶幊炭梢钥醋魇鞘褂靡幌盗袑?duì)象相互協(xié)作的軟件設(shè)計(jì)。 在 OOP 中,每個(gè)對(duì)象能夠接收消息,處理數(shù)據(jù)和發(fā)送消息給其他對(duì)象。每個(gè)對(duì)象都可以被看作是一個(gè)擁有清晰角色或責(zé)任的獨(dú)立小機(jī)器。

  • 可嘗試?yán)斫釳DN中介紹的面向?qū)ο蟮囊恍┬g(shù)語(yǔ)

Class 類
定義對(duì)象的特征。它是對(duì)象的屬性和方法的模板定義.
Object 對(duì)象
類的一個(gè)實(shí)例。
Property 屬性
對(duì)象的特征,比如顏色。
Method 方法
對(duì)象的能力,比如行走。
Constructor 構(gòu)造函數(shù)
對(duì)象初始化的瞬間, 被調(diào)用的方法. 通常它的名字與包含它的類一致.
Inheritance 繼承
一個(gè)類可以繼承另一個(gè)類的特征。
Encapsulation 封裝
一種把數(shù)據(jù)和相關(guān)的方法綁定在一起使用的方法.
Abstraction 抽象
結(jié)合復(fù)雜的繼承,方法,屬性的對(duì)象能夠模擬現(xiàn)實(shí)的模型。
Polymorphism 多態(tài)
多意為‘許多’,態(tài)意為‘形態(tài)’。不同類可以定義相同的方法或?qū)傩浴?/p>

其實(shí)面向?qū)ο笞詈诵牡乃膫€(gè)概念就是:抽象、繼承、封裝、多態(tài) 。

2、JavaScript中的面向?qū)ο缶幊?/h2>

首先我們就要來了解一下原型編程,因?yàn)镴S中實(shí)際上是弱類化的編程語(yǔ)言,是基于原型的而不是基于類的編程。

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

完整的內(nèi)容可看MDN 關(guān)于JS 面向?qū)ο蟮慕榻B。本文僅做基礎(chǔ)的介紹。

3、命名空間

命名空間是一個(gè)容器,它允許開發(fā)人員在一個(gè)獨(dú)特的,特定于應(yīng)用程序的名稱下捆綁所有的功能。 在JavaScript中,命名空間只是另一個(gè)包含方法,屬性,對(duì)象的對(duì)象。

注意:需要認(rèn)識(shí)到重要的一點(diǎn)是:與其他面向?qū)ο缶幊陶Z(yǔ)言不同的是,Javascript中的普通對(duì)象和命名空間在語(yǔ)言層面上沒有區(qū)別。比如var MySpace = {},從語(yǔ)言層面上,可以理解成創(chuàng)建了一個(gè)名為MySpace的對(duì)象,也可以理解為一個(gè)名為MySpace的命名空間,到底應(yīng)該理解成哪一種主要是看你如何使用它。

創(chuàng)造的JavaScript命名空間背后的想法很簡(jiǎn)單:一個(gè)全局對(duì)象被創(chuàng)建,所有的變量,方法和功能成為該對(duì)象的屬性。使用命名空間也最大程度地減少應(yīng)用程序的名稱沖突的可能性。

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

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

4、類

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

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

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

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

5、構(gòu)造函數(shù)

面向?qū)ο缶幊痰牡谝徊?,就是要生成?duì)象。前面說過,對(duì)象是單個(gè)實(shí)物的抽象。通常需要一個(gè)模板,表示某一類實(shí)物的共同特征,然后對(duì)象根據(jù)這個(gè)模板生成。

典型的面向?qū)ο缶幊陶Z(yǔ)言(比如 C++ 和 Java),都有“類”(class)這個(gè)概念。所謂“類”就是對(duì)象的模板,對(duì)象就是“類”的實(shí)例。但是,JavaScript 語(yǔ)言的對(duì)象體系,不是基于“類”的,而是基于構(gòu)造函數(shù)(constructor)和原型鏈(prototype)。

JavaScript 語(yǔ)言使用構(gòu)造函數(shù)(constructor)作為對(duì)象的模板。所謂”構(gòu)造函數(shù)”,就是專門用來生成實(shí)例對(duì)象的函數(shù)。它就是對(duì)象的模板,描述實(shí)例對(duì)象的基本結(jié)構(gòu)。一個(gè)構(gòu)造函數(shù),可以生成多個(gè)實(shí)例對(duì)象,這些實(shí)例對(duì)象都有相同的結(jié)構(gòu)。

構(gòu)造函數(shù)就是一個(gè)普通的函數(shù),但是有自己的特征和用法。如:

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

上面代碼中,Vehicle就是構(gòu)造函數(shù)。為了與普通函數(shù)區(qū)別,構(gòu)造函數(shù)名字的第一個(gè)字母通常大寫。

構(gòu)造函數(shù)的特點(diǎn)有兩個(gè):

  • 函數(shù)體內(nèi)部使用了this關(guān)鍵字,代表了所要生成的對(duì)象實(shí)例。
  • 生成對(duì)象的時(shí)候,必須使用new命令。

下面將詳細(xì)介紹一下new命令和this關(guān)鍵字。

6、new 命令

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

new命令的作用,就是執(zhí)行構(gòu)造函數(shù),返回一個(gè)實(shí)例對(duì)象。如:

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

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

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

  • 創(chuàng)建臨時(shí)對(duì)象
  • 將這個(gè)臨時(shí)對(duì)象賦值給函數(shù)內(nèi)部的this關(guān)鍵字。
  • Viehicle.prototype = { constructor: 構(gòu)造函數(shù) }
  • 臨時(shí)對(duì)象.__proto__ = Vehicle.prototype
  • 執(zhí)行 Vehicle.apply(this,arguments)
  • return thisreturn 創(chuàng)建的臨時(shí)對(duì)象
來自方方的示意圖

7、this 關(guān)鍵字

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

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

舉例說明:

  • 首先來看如何將普通的函數(shù)調(diào)用轉(zhuǎn)換為call的形式:
func(p1, p2) // 等價(jià)于
func.call(undefined, p1, p2)

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

此時(shí)this即為undefined,但注意:
如果你的call傳的第一個(gè)參數(shù)是 null 或者 undefined,那么 window 對(duì)象就是默認(rèn)的this(嚴(yán)格模式下默認(rèn)為 undefined

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

本例中的this即為obj

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

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

arr[0]() 
假想為    arr.0()
然后轉(zhuǎn)換為 arr.0.call(arr)
那么里面的 this 就是 arr 了 :)
  • 例4:(下面三個(gè)例子都是一些常見的經(jīng)過封裝后的函數(shù)的this,這些函數(shù)就無法轉(zhuǎn)化成call的形式了,需要看文檔才能知道,我?guī)痛蠹铱戳宋臋n,所以下面的例子就需要單獨(dú)記憶啦)
button.onclick = function(){
  console.log(this)
}
// 這里的 this 指的是 觸發(fā)事件的元素 即 button
  • 例5:
button.addEventListener('click',function(){
  console.log(this)
})
// 這里的 this 指的是 觸發(fā)事件的元素的引用 即 button
  • 例6:(jQuery)
$('ul').on('click','li',function(){
  console.log(this)
})
// 這里的 this 指的是 正在執(zhí)行事件的元素 即 li
  • 例7:下面三個(gè)例子就比較繞啦
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)

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

  • 例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、做幾個(gè)小練習(xí)吧

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

} // 構(gòu)造函數(shù)結(jié)束

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

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

human2 和 human 類似。

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

} // 構(gòu)造函數(shù)結(jié)束

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. 填空(本題不給答案,不確定的可以直接在控制臺(tái)測(cè)試):
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 中如何用函數(shù)模擬一個(gè)類?
  • 參考答案:
    ES 5 沒有 class 關(guān)鍵字,所以只能使用函數(shù)來模擬類。代碼如下:
    function Human(name){
      this.name = name
    }
    Human.prototype.run = function(){}
    
    var person = new Human('enoch')
    
    
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容