1. 程式人生 > 其它 >JavaScript高階程式設計--資料型別(2)_Object

JavaScript高階程式設計--資料型別(2)_Object

技術標籤:JavaScriptjsobject繼承建立物件深拷貝

文章較長,建議收藏以便瀏覽

《JavaScript高階程式設計(第三版)》學習總結

Object

  Object型別即物件型別,在ECMAScript中,物件其實就是一組資料和功能的集合。我們可以使用new操作符來建立:

let obj = new Object()

單單這一行程式碼,就可以引申出一系列疑問。建立的物件有什麼屬性和方法?new操作符幹了啥?建立物件有哪些方式?既然是物件怎麼實現繼承?怎麼實現物件深拷貝?and so on.如果一下子全都能說明白透徹的話,我斑願稱你為最強 。接下來我們挨個來剖析一下這些問題。

建立的物件有什麼屬性和方法?

我們在控制檯列印一下剛才宣告的obj例項,得到以下結果,在這裡就又得引入原型和原型鏈的概念,我就不做過多闡述了,可以參考JS中的原型和原型鏈(圖解)。下面說一下里面的具體屬性。
在這裡插入圖片描述

  • constructor:儲存著用於建立當前物件的函式。在這裡及是Object()。
  • hasOwnProperty(propertyName):用於檢查給定的屬性在當前物件例項中是否存在。其中,作為引數的屬性名必須以字串的形式指定(eg: obj.hasOwnProperty("name"))。
  • isPrototypeOf(object):用於檢查傳入的物件是否是另一個物件的原型。
  • propertyIsEnumerable(propertyName):用於檢查給定的屬性在當前物件例項中是否能夠使用for-in來枚舉出來,可以用來區分例項屬性和原型屬性。
  • toLocaleString():返回物件的字串表示,該字串與執行環境的地區對應。
  • toString():返回物件的字串表示。
  • valueOf():返回物件的字串、數值或布林值表示。通常與toString()方法的返回值相同。JS中toString()、toLocaleString()、valueOf()的區別

以上是官方介紹Object的每個例項都具有的屬性和方法,但是控制檯列印的不僅僅只有這些,還有下面的,我們繼續看:

  • _ _defineGetter _ _
    :給物件新增getter方法
  • _ _defineSetter _ _:給物件新增setter方法
  • _ _lookupGetter _ _:返回getter的定義函式
  • _ _lookupSetter _ _:返回setter的定義函式
let obj =  new Object()
// name新增getter
obj.__defineGetter__('name', ()=>this.name)
console.log(obj.name) // null
obj.name = "luffy"
// 此時name還沒有setter方法,因此不能賦值
console.log(obj.name) // null
// name新增setter
obj.__defineSetter__('name',(name)=>(this.name = name))
obj.name = "luffy"
// 此時可以看到name賦值成功
console.log(obj.name) // luffy
// 返回相應的定義函式
console.log(obj.__lookupGetter__('name')) // ()=>this.name
console.log(obj.__lookupSetter__('name')) // (name)=>(this.name = name)
  • get _ _ proto _ _:獲取_ _proto _ _指向,用法就是obj. __ proto __
  • set _ _ proto _ _:設定_ _proto _ _指向,用法就是obj. __ proto __ = 物件

new操作符幹了啥?

  1. 建立一個空物件
    let obj = {}
    
  2. 將空物件的__proto__指向建構函式的prototype,讓空物件繼承建構函式的原型
    obj.__proto__ = Object.prototype
    
  3. 將建構函式的this指向新物件,並執行建構函式為新物件新增屬性
    Object.call(obj)
    
  4. 返回新物件
    return obj
    

建立物件有哪些方式?

建立單個物件的話採用以下兩個方式即可:

  • new操作符+Object建構函式

    let obj = new Object()
    obj.name = "luffy"
    
  • 物件字面量

    let obj = {
    	name: "luffy"
    }
    

但是平常敲程式碼的業務中,往往需要建立很多物件,如果使用以上兩種方法的話,會產生大量重複程式碼,產生程式碼冗餘問題,因此以下幾種方法我們也需要掌握:

  • 工廠模式:用函式來封裝以特定的介面建立物件的細節
    • 缺點:沒有解決物件識別的問題(即沒辦法知道一個物件的型別)
function createObj(name){
	let obj = new Object()
	obj.name = name
	obj.sayName = () =>{
		console.log(this.name)
	}
	return obj
}
let obj = createObj("luffy")
  • 建構函式模式
    • 和工廠模式的不同之處:沒有顯示地建立物件;直接將屬性和方法賦給了this物件;沒有return語句。
    • 缺點:每個方法都要在每個例項上重新建立一遍,即下面的sayName(),可以通過把函式定義轉移到建構函式外部來解決這個問題。
function ObjFn(name){
	this.name = name
	this.sayName = ()=>{
		console.log(this.name)
	}
}
let obj = new ObjFn("luffy")
//不通過new呼叫的話,它就是一個普通函式,
//那麼執行的屬性和方法在瀏覽器中就會指向window物件,
//就能得到window.sayName()的結果為"who"
ObjFn("who") 

// 優化版
function ObjFn(name){
	this.name = name
	this.sayName = sayName
}
const sayName = ()=>{
	console.log(this.name)
}
  • class語法糖(需要與時俱進不是(っ•̀ω•́)っ✎⁾⁾ )
class Obj { // 注意命名規範
	constructor(name){
		this.name = name
	}
	sayName(){
		console.log(this.name)
	}
}
let obj = new Obj("luffy")
  • 原型模式
    • 優點:不必在建構函式中定義物件例項的資訊,而是可以將這些資訊直接新增到物件原型中。
// 初始寫法
function Obj() {
	Obj.prototype.name = "luffy"
	Obj.prototype.sayName = () =>{
		console.log(this.name)
	}
}
let obj1 = new Obj()
let obj2 = new Obj()
obj1.name = "Tom" // 不會修改原型物件的name屬性,而是例項物件添加了一個同名屬性
obj1.sayName() // Tom,訪問例項物件的name屬性,遮蔽原型物件中的同名屬性
obj2.sayName() // luffy,訪問原型物件的name屬性
// 進階寫法
function Obj(){}
Obj.prototype = {
	// constructor: Obj, // 設定constructor屬性的指向
	name: "luffy",
	sayName: ()=>{
		console.log(this.name)
	}
}
  • 組合使用建構函式模式和原型模式
    • 建構函式模式用於定義例項屬性,原型模式用於定義方法和共享的屬性。最大限度節省了記憶體,還支援向建構函式傳遞引數。
function Obj(name){
	this.name = name
}
Obj.prototype = {
	constructor: Obj,
	sayName: ()=>{
		console.log(this.name)
	}
}
let obj = new Obj("luffy")
  • 寄生建構函式模式
    • 就是相當於使用new來呼叫了工廠模式,返回的物件與建構函式以及建構函式的原型屬性之間沒有關係,酌情使用!
function createObj(name){
	let obj = new Object()
	obj.name = name
	obj.sayName = () =>{
		console.log(this.name)
	}
	return obj
}
let obj = new createObj("luffy")
  • 穩妥建構函式模式
    • 只能呼叫建構函式內的方法去訪問其資料成員。
function createObj(name){
	let obj = new Object()
	obj.sayName = () =>{
		console.log(name)
	}
	return obj
}
let obj = new createObj("luffy")
obj.sayName() // luffy

既然是物件怎麼實現繼承?

  • 原型鏈
    • 缺點:包含引用型別值的原型會被所有例項共享;在建立子型別的例項時,不能向超型別的建構函式中傳遞引數。
function Animal(){
    this.ages = [1,2,3]
}
function Cat(){
}
Cat.prototype = new Animal()
let cat1 = new Cat()
let cat2 = new Cat()
console.log(cat1.ages) //[ 1, 2, 3 ]
console.log(cat2.ages)//[ 1, 2, 3 ]
cat1.age.push(4) // 修改cat1會影響cat2
console.log(cat1.ages)//[ 1, 2, 3, 4 ]
console.log(cat2.ages)//[ 1, 2, 3, 4 ]
  • 借用建構函式
    • 缺點:函式複用問題
function Animal(name){
	this.name = name
    this.ages = [1,2,3]
}
function Cat(){
	Cat.call(this, "yoyo")
}
  • 組合繼承
    • 缺點:無論什麼時候都會呼叫兩次超型別的建構函式,一次是建立子型別原型的時候,一次是在建構函式內部。
function Animal(name){
	this.name = name
    this.ages = [1,2,3]
    this.sayName = () =>{
		console.log(this.name)
	}
}
function Cat(){
	Cat.call(this, "yoyo") // 繼承屬性
}
Cat.prototype = new Animal() // 繼承方法
  • 原型式繼承
    • ES5新增的Object.create()方法規範化了原型式繼承。
let Obj = {
	name: "luffy"
} 
let obj1 = Object.create(Obj) // 一個引數
let obj2 = Object.create(Obj,{age:{value: 23}}) // 兩個引數
  • 寄生式繼承
let Obj = {
	name: "luffy"
} 
function createObj(origin){
	let clone = new Object(origin)
	clone.sayName = (name) =>{ // 可以增強物件
		console.log(name)
    }
    return clone
}
let obj = createObj(Obj)
obj.sayName(obj.name) // luffy
  • 寄生組合式繼承
function inheritPrototype(subType,superType){
	let prototype1 = new Object(superType.prototype) // 建立物件
	prototype1.constructor = subType // 增強物件
	subType.prototype = prototype1 // 繫結物件
}
function Animal(name){
	this.name = name
    this.ages = [1,2,3]
}
Animal.prototype.sayName = () =>{
	console.log(this.name)
}
function Cat(){
	Cat.call(this, "yoyo") // 繼承屬性
}
inheritPrototype(Cat, Animal)
  • class對應的extends繼承
    • 不可繼承常規物件(eg: let Animal={...})
class Animal {
	constructor(){
		this.ages = [1,2,3]
	}
}
class Cat extends Animal{
	constructor(){
		super()
	}
}

怎麼實現物件深拷貝?

預設情況下物件之間的直接賦值都是淺拷貝,一個物件的屬性如果是基本資料型別, 那麼也都是深拷貝,如果物件的屬性包含了引用資料型別, 才真正的區分深拷貝和淺拷貝。

  • 物件的拷貝:將一個物件賦值給另外一個物件
  • 淺拷貝:將A物件賦值給B物件,修改B物件的屬性和方法會影響到A物件的屬性和方法
let obj = {a:1,b:2,c:[1,2]}
let obj1 = {}
for(let key in obj){
    obj1[key] = obj[key]
} // 等同於 Object.assign(obj1,obj)
obj1.c.push(3);
console.log(obj.c) // [1,2,3]
  • 深拷貝:將A物件賦值給B物件,修改B物件的屬性和方法不會影響到A物件的屬性和方法(附上一個大佬實現的包含所有型別的深拷貝方法)
const deepClone = (obj, hash = new WeakMap())=> {
    if (obj === null) return obj
    if (obj instanceof Date) return new Date(obj)
    if (obj instanceof RegExp) return new RegExp(obj)
    if (typeof obj !== 'object') return obj
    if (hash.get(obj)) return hash.get(obj)
    let cloneObj = new obj.constructor()
    hash.set(obj, cloneObj)
    for (let key in obj) {
      if (obj.hasOwnProperty(key)) {
        cloneObj[key] = this.deepClone(obj[key], hash)
      }
    }
    return cloneObj
}

關於Object的知識點,真是三天三夜都聊不完,就到這裡吧。