您好,登錄后才能下訂單哦!
小編給大家分享一下js對象有什么用,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
JavaScript作為一個基于對象(沒有類的概念)的語言,從入門到精通到放棄一直會被對象這個問題圍繞。
平時發的文章基本都是開發中遇到的問題和對最佳解決方案的探討,終于忍不住要寫一篇基礎概念類的文章了。
本文探討以下問題,在座的朋友各取所需,歡迎批評指正:
1、創建對象
2、__proto__與prototype
3、繼承與原型鏈
4、對象的深度克隆
5、一些Object的方法與需要注意的點
6、ES6新增特性
下面反復提到實例對象和原型對象,通過構造函數 new 出來的本文稱作 實例對象,構造函數的原型屬性本文稱作 原型對象。
創建對象
字面量的方式:
var myHonda = {color: "red", wheels: 4, engine: {cylinders: 4, size: 2.2}}
就是new Object()的語法糖,一樣一樣的。
工廠模式:
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()包了層皮,方便量產,并沒有本質區別,姑且算作創建對象的一種方式。
構造函數:
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() 傳入需要獲取原型對象的實例對象。
我自己都感覺說的有點亂,但是他們就是這樣的,上一張圖,看看能不能幫你更深刻理解這三者關系。
繼承與原型鏈
當訪問一個對象的屬性時,如果在對象本身找不到,就會去搜索對象的原型,原型的原型,知道原型鏈的盡頭 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。
以上是“js對象有什么用”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。