js對象詳解(JavaScript對象深度剖析,深度理解js對象)

js對象詳解(JavaScript對象深度剖析,深度理解js對象)

這算是醞釀好久的一篇文章了。
JavaScript做爲一個基於對象(沒有類的概念)的語言,從入門到精通到放棄一直會被對象這個問題圍繞。
平時發的文章基本都是開發中遇到的問題和對最佳解決方案的探討,終於忍不住要寫一篇基礎概念類的文章了。
本文探討如下問題,在座的朋友各取所需,歡迎批評指正:javascript

  1. 建立對象
  2. __proto__與prototype
  3. 繼承與原型鏈
  4. 對象的深度克隆
  5. 一些Object的方法與須要注意的點
  6. ES6新增特性

下面反覆提到實例對象和原型對象,經過構造函數 new 出來的本文稱做 實例對象,構造函數的原型屬性本文稱做 原型對象。java

建立對象

  • 字面量的方式:
var myHonda = {color: "red", wheels: 4, engine: {cylinders: 4, size: 2.2}}

就是new Object()的語法糖,同樣同樣的。es6

  • 工廠模式:
function createCar(){    
  var oTemp = new Object();    
  oTemp.name = arguments[0];
  //直接給對象添加屬性,每一個對象都有直接的屬性    
  oTemp.age = arguments[1];    
  oTemp.showName = function () {        
    alert(this.name);    
  };//每一個對象都有一個 showName 方法版本    
  return oTemp;
};
var myHonda = createCar('honda', 5)

只是給new Object()包了層皮,方便量產,並無本質區別,姑且算做建立對象的一種方式。json

  • 構造函數:
function Person(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
  this.getName = function() {
    return this.name;
  };
}
var rand = new Person("Rand McKinnon", 33, "M");

上面構造函數的 getName 方法,每次實例化都會新建該函數對象,還造成了在當前狀況下並無卵用的閉包,因此構造函數添加方法用下面方式處理,工廠模式給對象添加方法的時候也應該用下面的方式避免重複構造函數對象數組

function Person(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
  this.getName = getName
}
function getName() {
  return this.name;
};

構造函數建立對象的過程和工廠模式又是半斤八兩,至關於隱藏了建立新對象和返回該對象這兩步,構造函數內 this 指向新建對象,沒什麼不一樣。
最大不一樣點: 構造函數創造出來的對象 constructor 屬性指向該構造函數,工廠模式指向 function Object(){...}
構造函數至關於給原型鏈上加了一環,構造函數有本身的 prototype,工廠模式就是個普通函數。說到這兒我上一句話出現了漏洞,工廠模式的 constructor 指向哪得看第一句話 new 的是什麼。
構造函數直接調用而不 new 的話,就看調用時候 this 指向誰了,直接調用就把屬性綁到 window 上了,經過 call 或者 apply 綁定到其餘對象做用域就把屬性添加到該對象了。瀏覽器

  • 原型模式:
    構造函數雖然在原型鏈上加了一環,但顯然這一環啥都沒有啊,這樣一來和工廠模式又有什麼區別?加了一環又有什麼意義?原型模式浮出水面。
function Car(){} 
//用空構造函數設置類名
Car.prototype.color = "blue";//每一個對象都共享相同屬性
Car.prototype.doors = 3;
Car.prototype.drivers = new Array("Mike","John");
Car.prototype.showColor = function(){        
  alert(this.color);
};//每一個對象共享一個方法版本,省內存。
//構造函數的原型屬性能夠經過字面量來設置,別忘了經過 Object.defineProperty()設置 constructor 爲該構造函數
function Car(){} 
Car.prototype = {
  color:"blue",
  doors:3,
  showColor:function(){        
    alert(this.color);
  }
}
Object.defineProperty(Car.prototype, "constructor", { enumerable:false, value:Car })
//(不設置 constructor 會致使 constructor 不指向構造函數,直接設置 constructor 會致使 constructor 可枚舉)

使用原型模式注意動態性,經過構造函數實例化出的對象,他的原型對象是構造函數的 prototype ,若是在他的原型對象上增長或刪除一些方法,該對象會繼承這些修改。例如,先經過構造函數 A 實例化出對象 a ,而後再給 A.prototype 添加一個方法,a 是能夠繼承這個方法的。可是給 A.prototype 設置一個新的對象,a 是不會繼承這個新對象的屬性和方法的。聽起來有點繞,修改 A.prototype 至關於直接修改 a 的原型對象,a 很天然的會繼承這些修改,可是從新給 A.prototype 賦值的話,修改的是構造函數的原型,並無影響 a 的原型對象!a 被建立出來之後原型對象就已經肯定了,除非直接修改這個原型對象(或者這個原型對象的原型對象),不然 a 是不會繼承這些修改的!安全

  • Object.create()
    傳入要建立對象實例的原型對象,和原型模式幾乎是一個意思也是至關於在原型鏈上加了一環,區別在於這種方式建立的對象沒有構造函數。這種方式至關於:
function object(o){
    function F(){}
    F.prototype = o;
    return new F()
}

至關於構造函數只短暫的存在了一會,建立出來的對象的 constructor 指向 原型對象 o 的 constructor !閉包

  • 混合模式:
    使用原型模式時,當給實例對象設置本身專屬的屬性的時候,該實例對象會忽略原型鏈中的該屬性。但當原型鏈中的屬性是引用類型值的時候,操做不當有可能會直接修改原型對象的屬性!這會影響到全部使用該原型對象的實例對象!
    大部分狀況下,實例對象的多數方法是共有的,多數屬性是私有的,因此屬性在構造函數中設置,方法在原型中設置是合適的,構造函數與原型結合使用是一般的作法。
    還有一些方法,無非是工廠模式與構造函數與原型模式的互相結合,在生成過程和 this 指向上作一些小變化。
  • class 方式:
    見下面 ES6 class 部分,只是一個語法糖,本質上和構造函數並無什麼區別,可是繼承的方式有一些區別。

proto與prototype

這兩個究竟是什麼關係?搞清楚 實例對象 構造函數 原型對象 的三角關係,這兩個屬性的用法就天然清晰了,順便說下 constructor。
構造函數建立的實例對象的 constructor 指向該構造函數(但實際上 constructor 是對應的原型對象上的一個屬性!因此實例對象的 constructor 是繼承來的,這一點要注意,若是利用原型鏈繼承,constructor 將有可能指向原型對象的構造函數甚至更上層的構造函數,其餘重寫構造函數 prototype 的行爲也會形成 constructor 指向問題,都須要重設 constructor),構造函數的 prototype 指向對應的原型對象,實例對象的 __proto__ 指對應的原型對象,__proto__是瀏覽器的實現,並無出如今標準中,能夠用 constructor.prototype 代替。考慮到 Object.create() 建立的對象,更安全的方法是 Object.getPrototpyeOf() 傳入須要獲取原型對象的實例對象。
我本身都感受說的有點亂,可是他們就是這樣的,上一張圖,看看能不能幫你更深入理解這三者關係。
三角關係app

繼承與原型鏈

當訪問一個對象的屬性時,若是在對象自己找不到,就會去搜索對象的原型,原型的原型,直到原型鏈的盡頭 null,那原型鏈是怎麼鏈起來的?
把 實例對象 構造函數 原型對象 視爲一個小組,上面說了三者互相之間的關係,構造函數是函數,可實例對象和原型對象可都是普通對象啊,這就出現了這樣的狀況:
這個小組的原型對象,等於另外一個小組實例對象,而此小組的原型對象又多是其餘小組的實例對象,這樣一個個的小組不就鏈接起來了麼。舉個例子:函數

function Super(){
  this.val = 1;
  this.arr = [1];
}
function Sub(){
  // ...
}
Sub.prototype = new Super();

Sub 是一個小組 Super 是一個小組,Sub 的原型對象連接到了 Super 的實例對象。
基本上全部對象順着原型鏈爬到頭都是 Object.prototype , 而 Object.prototype 就沒有原型對象,原型鏈就走到頭了。
判斷構造函數和原型對象是否存在於實例對象的原型鏈中:
實例對象 instanceof 構造函數,返回一個布爾值,原型對象.isPrototypeOf(實例對象),返回一個布爾值。
上面是最簡單的繼承方式了,可是有兩個致命缺點:

  • 全部 Sub 的實例對象都繼承自同一個 Super 的實例對象,我想傳參數到 Super 怎麼辦?
  • 若是 Super 裏有引用類型的值,好比上面例子中我給 Sub 的實例對象中的 arr 屬性 push 一個值,豈不是牽一髮動全身?
    下面說一種最經常使用的組合繼承模式,先舉個例子:
function Super(value){
  // 只在此處聲明基本屬性和引用屬性
  this.val = value;
  this.arr = [1];
}
//  在此處聲明函數
Super.prototype.fun1 = function(){};
Super.prototype.fun2 = function(){};
//Super.prototype.fun3...
function Sub(value){
  Super.call(this,value);   // 核心
  // ...
}
Sub.prototype = new Super();    // 核心

過程是這樣的,在簡單的原型鏈繼承的基礎上, Sub 的構造函數裏運行 Super ,從而給 Sub 的每個實例對象一份單獨的屬性,解決了上面兩個問題,能夠給 Super 傳參數了,並且由於是獨立的屬性,不會由於誤操做引用類型值而影響其餘實例了。不過還有個小缺點: Sub 中調用的 Super 給每一個 Sub 的實例對象一套新的屬性,覆蓋了繼承的 Super 實例對象的屬性,那被覆蓋的的那套屬性不就浪費了?豈不是白繼承了?最嚴重的問題是 Super 被執行了兩次,這不能忍(其實也沒多大問題)。下面進行一下優化,把上面例子最後一行替換爲:

Sub.prototype = Object.create(Super.prototype);
// Object.create() 給原型鏈上添加一環,不然 Sub 和 Super 的原型就重疊了。
Sub.prototype.constructor = Sub;

到此爲止,繼承很是完美。
其餘還有各路繼承方式無非是在 簡單原型鏈繼承 --> 優化的組合繼承 路程之間的一些思路或者封裝。

  • 經過 class 繼承的方式:
    經過 class 實現繼承的過程與 ES5 徹底相反,詳細見下面 ES6 class的繼承 部分。

對象的深度克隆

JavaScript的基礎類型是值傳遞,而對象是引用傳遞,這致使一個問題:
克隆一個基礎類型的變量的時候,克隆出來的的變量是和舊的變量徹底獨立的,只是值相同而已。
而克隆對象的時候就要分兩種狀況了,簡單的賦值會讓兩個變量指向同一塊內存,二者表明同一個對象,甚至算不上克隆克隆。但咱們經常須要的是兩個屬性和方法徹底相同但卻徹底獨立的對象,稱爲深度克隆。咱們接下來討論幾種深度克隆的方法。
說幾句題外的話,業界有一個很是知名的庫 immutable ,我的認爲很大程度上解決了深度克隆的痛點,咱們修改一個對象的時候,不少時候但願獲得一個全新的對象(好比Redux每次都要用一個全新的對象修改狀態),由此咱們就須要進行深度克隆。而 immutable 至關於產生了一種新的對象類型,每一次修改屬性都會返回一個全新的 immutable 對象,免去了咱們深度克隆的工做是小事,關鍵性能特別好。

  • 歷遍屬性
function clone(obj){
  var newobj = obj.constructor === Array ? [] : {};  // 用 instanceof 判斷也可
  if(typeof obj !== 'object'  || obj === null ){
    return obj
  } else {
    for(var i in obj){
      newobj[i] = typeof obj[i] === 'object' ? cloneObj(obj[i]) : obj[i]; 
      // 只考慮 對象和數組, 函數雖然也是引用類型,但直接賦值並不會產生什麼反作用,因此函數類型無需深度克隆。
    }
  }
  return newobj;
};
  • 原型式克隆
function clone(obj){
  function F() {};
  F.prototype = obj;
  var f = new F();
  for(var key in obj)
  {
    if(typeof obj[key] =="object")
    {
      f[key] = clone(obj[key])
    }
  }
  return f ;
}

這種方式不能算嚴格意義上的深度克隆,並無切斷新對象與被克隆對象的聯繫,被克隆對象做爲新對象的原型存在,雖然新對象的改變不會影響舊對象,但反之則否則!並且給新對象屬性從新賦值的時候只是覆蓋了原型中的屬性,在歷遍新對象的時候也會出現問題。這種方式問題重重,除了實現特殊目的能夠酌情使用,一般狀況應避免使用。

  • json序列化
var newObj = JSON.parse(JSON.stringify(obj));

這是我最喜歡的方式了!簡短粗暴直接!可是最大的問題是,畢竟JSON只是一種數據格式因此這種方式只能克隆屬性,不能克隆方法,方法在序列化之後就消失了。。。

一些Object的方法與須要注意的點

Object 自身的方法:

  • 設置屬性,Object.defineProperty(obj, prop, descriptor) 根據 descriptor 定義 obj 的 prop 屬性(值,是否可寫可枚舉可刪除等)。
    Object.getOwnPropertyDescriptor(obj, prop) 返回 obj 的 prop 屬性的描述。
  • 使對象不可拓展,Object.preventExtensions(obj),obj 將不能添加新的屬性。
    判斷對像是否可拓展,Object.isExtensible(obj)
  • 密封一個對象,Object.seal(obj),obj 將不可拓展且不能刪除已有屬性。
    判斷對象是否密封,Object.isSealed(obj)
  • 凍結對象,Object.freeze(obj) obj 將被密封且不可修改。
    判斷對象是否凍結,Object.isFrozen(obj)
  • 獲取對象自身屬性(包括不可枚舉的),Object.getOwnPropertyNames(obj),返回 obj 全部自身屬性組成的數組。
    獲取對象自身屬性(不包括不可枚舉的),Object.keys(obj),返回 obj 全部自身可枚舉屬性組成的數組。
    當使用for in循環遍歷對象的屬性時,原型鏈上的全部可枚舉屬性都將被訪問。
    只關心對象自己時用Object.keys(obj)代替 for in,避免歷遍原型鏈上的屬性。
  • 獲取某對象的原型對象,Object.getPrototypeOf(object),返回 object 的原型對象。
    設置某對象的原型對象,Object.setPrototypeOf(obj, prototype)ES6 新方法,設置 obj 的原型對象爲 prototype ,該語句比較耗時。

Object.prototype 上的方法:

  • 檢查對象上某個屬性是否存在時(存在於自己而不是原型鏈中),obj.hasOwnProperty() 是惟一可用的方法,他不會向上查找原型鏈,只在 obj 自身查找,返回布爾值。
  • 檢測某對象是否存在於參數對象的原型鏈中,obj.isPrototypeOf(obj2),obj 是否在 obj2 的原型鏈中,返回布爾值。
  • 檢測某屬性是不是對象自身的可枚舉屬性,obj.propertyIsEnumerable(prop),返回布爾值。
  • 對象類型,obj.toString(),返回 "[object type]" type 能夠是 Date,Array,Math 等對象類型。
  • obj.valueOf(),修改對象返回值時的行爲,使用以下:
function myNumberType(n) {
    this.number = n;
}
myNumberType.prototype.valueOf = function() {
    return this.number;
};
myObj = new myNumberType(4);
myObj + 3; // 7

ES6新增特性

  • 判斷兩個值是否徹底相等,Object.is(value1, value2),相似於 === 可是能夠用來判斷 NaN。
  • 屬性和方法簡寫:
// 屬性簡寫
var foo = 'bar';
var baz = {foo};
baz // {foo: "bar"}
// 等同於
var baz = {foo: foo};
// 方法簡寫
function f(x, y) {
  return {x, y};
}
// 等同於
function f(x, y) {
  return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}
  • 合併對象:
    Object.assign(target, [...source]);將 source 中全部和枚舉的屬性複製到 target。
    多個 source 對象有同名屬性,後面的覆蓋前面的。
var target = { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

注意一點,該命令執行的是淺克隆,若是 source 中有屬性是對象,target 中會複製該對象的引用。
經常使用於給對象添加屬性和方法(如給構造函數的原型添加方法),克隆、合併對象等。

  • 獲取對象自身的值或鍵值對(作爲Object.keys(obj)的補充不包括不可枚舉的):
    Object.keys(obj)返回 obj 自身全部可枚舉屬性的值組成的數組。
    Object.entries(obj)返回 obj 自身全部可枚舉鍵值對數組組成的數組,例如:
var obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
// 可用於將對象轉爲 Map 結構
var obj = { foo: 'bar', baz: 42 };
var map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }
  • 拓展運算符:
    取出對象全部可歷遍屬性,舉例:
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }
// 可代替 Object.assign()
let ab = { ...a, ...b };
// 等同於
let ab = Object.assign({}, a, b);

可用於解構賦值中最後一個參數:

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
// 能夠這樣理解,把 z 拆開之後就等於後面對象未被分配出去的鍵值對。
  • Null 傳導運算符:
const firstName = message?.body?.user?.firstName || 'default';
// 代替
const firstName = (message
  && message.body
  && message.body.user
  && message.body.user.firstName) || 'default';
  • class:
    ES6 引入了 class 關鍵字,但並無改變對象基於原型繼承的原理,只是一個語法糖,讓他長得像傳統面嚮對象語言而已。
    如下兩個寫法徹底等價:
function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};
//定義類
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
// 類中定義的方法就是在原型上

有兩點區別, class 中定義的方法是不可枚舉的,class 必須經過 new 調用不能直接運行。
class 不存在變量提高,使用要在定義以後。
//
class 中的方法前加 static 關鍵字定義靜態方法,只能經過 class 直接調用不能被實例繼承。
若是靜態方法包含 this 關鍵字,這個 this 指的是 class,而不是實例。注意下面代碼:

class Foo {
  static bar () {
    this.baz();
  }
  static baz () {
    console.log('hello');
  }
  baz () {
    console.log('world');
  }
}
Foo.bar() // hello

父類的靜態方法,能夠被子類繼承,目前 class 內部沒法定義靜態屬性。
//
設置靜態屬性與實例屬性新提案:
class 的實例屬性能夠用等式,寫入類的定義之中。
靜態屬性直接前面加 static 便可。

class MyClass {
  myProp = 42;
  static myStaticProp = 42;
}
  • class 的繼承:
    class 經過 extends 實現繼承,注意 super 關鍵字
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 調用父類的constructor(x, y)
    this.color = color;
  }
  toString() {
    return this.color + ' ' + super.toString(); // 調用父類的toString()
  }
}

extends 能夠繼承其餘類或任何有 prototype 屬性的函數。
super 會從父類獲取各路信息綁定到子類的 this。
子類本身沒有 this 對象,要先繼承父類的實例對象而後再進行加工,因此要在 constructor 裏調用 super 繼承 this 對象後才能使用 this。
ES5 的繼承,實質是先創造子類的實例對象 this,而後再將父類的方法添加到 this 上面(Parent.apply(this))。ES6 的繼承機制徹底不一樣,實質是先創造父類的實例對象 this(因此必須先調用 super 方法建立和繼承這個 this,並綁定到子類的 this),而後再用子類的構造函數修改this。
這條理由也是形成了 ES6 以前沒法繼承原生的構造函數(Array Function Date 等)的原型對象,而使用 class 能夠。由於 ES5 中的方法是先實例化子類,再把父類的屬性添加上去,可是父類有不少不能直接訪問的屬性或方法,這就糟了,而經過 class 繼承反其道而行之先實例化父類,這就天然把全部屬性和方法都繼承了。
super 做爲對象時,在普通方法中,指向父類的原型對象;在靜態方法中,指向父類。
經過 super 調用父類的方法時,super 會綁定子類的 this。
constructor 方法會被默認添加:

class ColorPoint extends Point {
}
// 等同於
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}

Object.getPrototypeOf(object),獲取某對象的原型對象,也能夠獲取某類的原型類。

  • class 的 __proto__與prototype
    子類的__proto__屬性,表示構造函數的繼承,老是指向父類。
    子類prototype屬性的__proto__屬性,表示方法的繼承,老是指向父類的 prototype 屬性。
    至關於子類自己繼承父類,子類的原型對象繼承自父類的原型對象。
  • new.target: 用在構造函數或者 class 內部,指向調用時 new 的構造函數或者 class。
相關文章
相關標籤/搜索