前端基礎整理 | Javascript基礎 (二)對象與繼承

個人向,對JS知識進行了查漏補缺,主要來源于《JS高級程序設計》和網上博客,本文內容主要包括以下:

  1. 對象
  2. 創建對象
  3. 繼承

一、對象

特性(attribute),描述了屬性(property)的各種特征。內部使用,不能直接訪問,兩對方括號括起來。

1. 數據屬性:

  • 定義:包含一個數據值的位置,在這個位置可以對數據值進行讀寫。
  • 創建方法:定義對象的時候的鍵值對就是數據屬性啦。
  • 特性:
    • [[Configurable]] :表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或能否把屬性修改為訪問器屬性,默認為true。
    • [[Enumerable]] :表示能否通過for-in循環返回屬性,默認為 true
    • [[Writable]] :表示能否修改屬性的值,默認為 true。
    • [[Value]] :包含該屬性的數據值。默認為 undefined。
  • 設置屬性方法:
    Object.defineProperty(person, 'name', {
        configurable: true, //可以被刪除,可以修改特性,可以修改為訪問器屬性
        writable: false, //不可以寫入其他值
        enumerable: true, //可以for-in遍歷
        value: 'tony' //值是tony
    })
    Object.getOwnPropertyDescriptor(person,'name').configurable // 查看特性
    

?在使用defineProperty創建時候,未定義configurable / writable / enumerable都是默認false

2. 訪問器屬性

  • 創建方法:不能直接定義,只能通過Object.defineProperty()方法來定義。
  • 特性:
    • [[Configurable]] :表示能否通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或能否把屬性修改為訪問器屬性,默認為true。
    • [[Enumerable]] :表示能否通過for-in循環返回屬性,默認為true。
    • [[Get]] :讀取屬性調用的函數,默認undefined
    • [[Set]] :寫入屬性調用的函數,默認undefined
  • 設置屬性方法:
    var person = {
      _age:10,
      isAdult: false
    }
    Object.defineProperty(person, 'age', {
      get: function () { // 只指定getter那默認不能write只能read
        return this._age
      },
      set: function (val) { //只指定setter那默認不能read只能write
        this._age = val
        if (val > 18)
          this.isAldult = true
        else
          this.isAldult = false
      }  
    })
    
  • 定義多個屬性
    Object.defineProperties(book, {
      _name: {
        writable: true,
        configurable: true,
        value: 'tony'
      },
      name: {
        get: function(){},
        set: function(){}
      }
    })
    

二、創建對象

1. 工廠模式:

  • 優點:解決了創建多個相似對象;
  • 缺點:但問題是無法識別對象的類型。
function createPerson(name, age) {
  let o = new Object()
  o.name = name
  o.age = age
  o.sayName = function() { alert(o.name); }
  return o;
}

2. 構造函數模式

function Person(name,age){
  this.name=name
  this.age=age
  this.sayName = function() { alert(this.name); }
}

實際上,任何函數都可以是構造函數,只要配上new。而構造函數沒有用new來調用,也是一個普通函數。

  • 那么new做了什么呢?
  1. 創建一個新對象
  2. 把構造函數的作用域賦給新對象(this指向新對象)
  3. 執行構造函數代碼(給新對象添加屬性)
  4. 返回新對象

手動模擬一下new的工作:

function newPerson(name, age) {
  let o = new Object()
  Person.call(o, name, age)
  return o
}
tony = newPerson('tony', 10)
  • 缺點
    構造函數也存在問題:每個方法都在實例上重新創建一遍??梢杂眠@段代碼證明:console.log(person1.sayName === person2.sayName) // false。
    當然,我們可以在外部聲明函數,然后在構造函數中引用該函數。但是這么做會在全局作用域定義很多函數,封裝性大大降低。而原型模式能很好地解決這一點,因為它可以讓所有對象實例共享它所包含的屬性和方法。

3. 原型模式

  • 原型(prototype)是什么?
    prototype是一個指針,指向函數原型對象。prototype是一個函數的屬性。
    (理解原型的前提是要知道,函數本身也是一個對象,prototype是它的屬性之一,指向一個叫原型對象的東西)

  • 一張經典的圖片

    一張經典的圖片

  • prototype 與 __proto__
    當創建函數的時候,函數的原型對象自動獲得一個constructor屬性,該屬性指向這個函數。
    當創建實例的時候,實例內部也包括一個指針[[Prototype]],指向構造函數的原型對象。這個東西在chrome之類的瀏覽器實現為__proto__指針。在ES5中標準的拿實例對象原型的方法是Object.getPrototypeOf()

    三種原型對象的取法

  • 原型對象掛載方法和屬性
    我們可以向原型對象上掛屬性和方法,這樣每個使用這個原型的實例都能讀到。并且,我們解決了構造函數方法每次實例化都創建的問題。

    image.png

  • 判斷屬性方法

person.hasOwnProperty('name') //true因為這是來自實例的屬性。
person.hasOwnProperty('age') //false因為這是來自原型繼承來的屬性。
'age' in person //true 'in'操作符在對象能訪問該屬性時返回true
  • 獲取屬性方法
Object.keys(Person.prototype) //獲取[[Enumerable]]為true的可枚舉實例屬性
Object.getOwnPropertyNames() //獲取所有的實例屬性

Reflect.ownKeys(person) // 獲取所有的實例屬性以及symbol
Object.getOwnPropertyNames(person).concat(Object.getOwnPropertySymbols(person)) // 就是上面代碼的實際返回
  • 缺點
    原型對象的缺點是:省略了為構造函數傳遞初始化參數的環節,導致默認情況下取得相同屬性

4. 原型+構造函數模式

目前ECMAScript中最廣泛的模式,就是構造函數模式用來定義實例屬性,原型模式用來定義方法和共享的屬性。

5. 動態原型模式

其實就是在構造函數內弄了一個判斷語句,當不存在一個方法的時候,將方法掛在原型上。

function Person(name) {
  this.name = name
  if ( typeof this.sayName!='function') {
    Person.prototype.sayName = function() { alert(this.name)}
  }
}

6.寄生構造模式

代碼和工廠模式一樣,就是用new來創建,暫時不做分析

7.穩妥構造模式

有種閉包的感覺,不用this進行構造,沒有公共屬性,用在需要特殊的安全執行環境。

三、 繼承

1. 原型鏈

MDN文檔的描述再結合上面的"一張經典的圖片"食用更佳:
原型鏈的頂端是Object,再往上就是null了,null沒有原型。

JavaScript 對象是動態的屬性“包”(指其自己的屬性)。JavaScript 對象有一個指向一個原型對象的鏈。當試圖訪問一個對象的屬性時,它不僅僅在該對象上搜尋,還會搜尋該對象的原型,以及該對象的原型的原型,依次層層向上搜索,直到找到一個名字匹配的屬性或到達原型鏈的末尾。

2. 借用構造函數繼承

  • 核心:
    子類調用父類的構造函數從而實現屬性的繼承
  • 優點:
    1.可以向父類傳遞參數
    2.父類的引用屬性不會被子類實例共享
  • 缺點:
    1.父類方法不能復用
    2.對子類實例使用 instanceof 只會識別到子類
function Person(name){
  this.name = name
}
function Student(name){
  Person.call(this, name)
}

3. 原型鏈繼承

  • 核心:子類把prototype指向父類的一個實例對象
  • 優點:1.父類方法可復用;2. instanceof 可以識別到父類子類
  • 缺點:1.子類構建實例時不能傳參;2. 父類的引用屬性會被所有子類實例共享
function Person(name){
  this.name = name
}
person = new Person('tony')
function Student(){}
Student.prototype = person 
Student.prototype.constructor = Student

4. 原型鏈+構造函數 組合繼承

  • 核心:子類調用父類構造函數來實現屬性繼承,prototype指向父類的實例對象。
  • 優點:構造函數和原型鏈互補,即父類方法可復用 & 父類引用類型屬性不會被共享。
  • 缺點:構造函數調用了兩次,造成性能浪費,并且可能會覆蓋子類同名屬性。
function Person(name){
  this.name = name
}
Person.prototype.sayHi = function(){alert('hello')}
function Student(name){
  Person.call(this,name)
}
Student.prototype = new Person()
Student.prototype.constructor = Student

5. 原型式繼承

  • 核心:創建臨時構造函數,把傳入對象作為構造函數的原型,返回臨時類型的新實例。
function object(o){
  function F(){}
  F.prototype = o
  return new F()
}
let person = {
    name: 'tony'
};
let anotherPerson = object(person)

ECMAScript 5 通過新增 Object.create()方法規范化了原型式繼承。

所以上述可以簡化為 let anotherPerson = Object.create(person)

6. 寄生繼承

只是一種思路而已,沒什么優點,通過給使用原型式繼承獲得一個目標對象的淺復制,然后增強這個淺復制的能力。

function createAnother(original){ 
    var clone=object(original)
    clone.sayHi = function(){
        alert("hi");
    };
    return clone
}
var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person)
anotherPerson.sayHi()

7. 寄生組合式繼承

目前最完美的繼承方法,只需要在繼承函數中調用構造函數再使用下面的繼承就行了。

function inheritPrototype(subType, superType){
    var prototype = Object.create(superType.prototype); // 創建了父類原型的淺復制
    prototype.constructor = subType;             // 修正原型的構造函數
    subType.prototype = prototype;               // 將子類的原型替換為這個原型
}

為了方便理解,這里有兩個類似的繼承函數。第一個是使用類似原型構造的F函數,第二個是直觀的展示了繼承在Chrome等具有__proto__指針中的形式。

function F_inherits(Child, Parent) {
  var F = function() {}
  F.prototype = Parent.prototype
  Child.prototype = new F()
  Child.prototype.constructor = Child
}
function myInherits(Child, Parent) {
  Child.prototype = { constructor: Child, __proto__: Parent.prototype }
}

8. class 繼承(ES6)

ES6繼承的結果和寄生組合繼承相似,本質上,ES6繼承是一種語法糖。但是,寄生組合繼承是先創建子類實例this對象,然后再對其增強;而ES6先將父類實例對象的屬性和方法,加到this上面(所以必須先調用super方法),然后再用子類的構造函數修改this。

  • 語法:
class A {}
class B extends A {
  constructor() {
    super();
  }
}
  • 實現原理:
class A {}
class B {}
Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}
// B的實例繼承A的實例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 繼承 A 的靜態屬性
Object.setPrototypeOf(B, A);

ES6繼承與ES5繼承的異同:

  • 相同點:本質上ES6繼承是ES5繼承的語法糖
  • 不同點:
    1. ES6繼承中子類的構造函數的原型鏈指向父類的構造函數,ES5中使用的是構造函數復制,沒有原型鏈指向。
    2. ES6子類實例的構建,基于父類實例,ES5中不是。

四、一些自己實現的函數

幫助大家更好地理解:對象、繼承。

/**
 * call實現
 */
Function.prototype._call = function(ctx, ...args) {
  ctx = ctx || window
  ctx.func = this
  let result = ctx.func(...args)
  delete ctx.func
  return result
}
/**
 * apply實現
 */
Function.prototype._apply = function(ctx, args) {
  ctx = ctx || window
  ctx.func = this
  let result = ctx.func(...args)
  delete ctx.func
  return result
}
/**
 * bind實現
 */
Function.prototype._bind = function(target) {
  target = target || window
  const that = this
  const args = [...arguments].slice(1)
  let fn = function() {
    return that.apply(
      this instanceof fn ? this : target,
      args.concat(...arguments)
    )
  }
  let F = function() {}
  F.prototype = this.prototype
  fn.prototype = new F() // fn.prototype.__proto__ == this.prototype  true
  return fn
}
/**
 * instanceof實現
 */
function _instanceof(a, b) {
  let prototype = b.prototype
  let a = a.__proto__
  while (true) {
    if (a === null || a === undefined) {
      return false
    } else if (a === prototype) {
      return true
    } else {
      a = a.__proto__
    }
  }
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 博客內容:什么是面向對象為什么要面向對象面向對象編程的特性和原則理解對象屬性創建對象繼承 什么是面向對象 面向對象...
    _Dot912閱讀 1,447評論 3 12
  • ??面向對象(Object-Oriented,OO)的語言有一個標志,那就是它們都有類的概念,而通過類可以創建任意...
    霜天曉閱讀 2,135評論 0 6
  • 第3章 基本概念 3.1 語法 3.2 關鍵字和保留字 3.3 變量 3.4 數據類型 5種簡單數據類型:Unde...
    RickCole閱讀 5,149評論 0 21
  • JavaScript面向對象程序設計 本文會碰到的知識點:原型、原型鏈、函數對象、普通對象、繼承 讀完本文,可以學...
    moyi_gg閱讀 770評論 0 2
  • 函數和對象 1、函數 1.1 函數概述 函數對于任何一門語言來說都是核心的概念。通過函數可以封裝任意多條語句,而且...
    道無虛閱讀 4,614評論 0 5