JavaScript的對象
對象是JavaScript的一種資料類型。對象可以看成是屬性的無序集合,每個屬性都是一個鍵值對,屬性名是字元串,是以可以把對象看成是從字元串到值的映射。這種資料結構在其他語言中稱之為“散列(hash)”、“字典(dictionary)”、“關聯數組(associative array)”等。
原型式繼承:對象不僅僅是字元串到值的映射,除了可以保持自有的屬性,JavaScript對象還可以從一個稱之為原型的對象繼承屬性,對象的方法通常是繼承的屬性,這是JavaScript的核心特征。
JavaScript對象是動态的—可以新增屬性也可以删除屬性,但是他們常用來模拟靜态以及靜态類型語言中的“結構體”
建立對象
1、對象直接量
建立對象最簡單的方式就是在JavaScript代碼中使用對象直接量。
var book = {
"main title": 'guide', //屬性名字裡有空格,必須加引号
"sub-title": 'JS', //屬性名字裡有連字元,必須加引号
for: 'development', //for是關鍵字,不過從ES5開始,作為屬性名關鍵字和保留字可以不加引号
author: {
firstname: 'David', //這裡的屬性名就都沒有引号
surname: 'Flanagan'
}
}
注意: 從ES5開始,對象直接量中的最後一個屬性後的逗号将被忽略。
擴充: [
JavaScript中的關鍵字和保留字]
2、通過new建立對象
new 運算符建立并初始化一個新對象。關鍵字new後跟一個函數調用。這裡的函數稱做構造函數(constructor),構造函數用以初始化一個新建立的對象。JavaScript中的資料類型都包含内置的構造函數。
var o = new Object(); //建立一個空對象,和{}一樣。
var arr = new Array(); //建立一個空數組,和[]一樣。
擴充 1:new
new 是一個一進制運算符,專門運算函數的。new後面調用的函數叫做構造函數,構造函數new的過程叫做執行個體化。
當new去調用一個函數 : 這個時候函數中的this就指向建立出來的對象,而且函數的的傳回值直接就是this(隐式傳回)
有一個預設慣例就是構造函數的名字首字母大寫。
注意:
當return的時候,如果是後面為簡單類型,那麼傳回值還是這個對象;
如果return為對象類型,那麼傳回的就是return後面的這個對象。
擴充 2:基本類型和對象類型(複雜類型)的差別
指派:
基本類型 : 指派的時候隻是值的複制
對象類型 : 指派不僅是值的複制,而且也是引用的傳遞(可以了解為記憶體位址)可以了解為賦址。
比較相等
基本類型 : 值相同就可以
對象類型 : 值和引用都相同才行
擴充 3:原型 prototype
每一個JavaScript對象(null除外)都和另一個對象相關聯,這個對象就是原型,每一個對象都從原型繼承屬性。
3、Object.create()
Object.create() 這個方法是ES5定義的,它建立一個新對象,其中第一個參數是這個對象的原型。第二個參數是可選參數,用以對對象屬性進行進一步描述。
可以通過傳入參數 null 建立一個沒有原型的新對象,不過這個新對象不會繼承任何東西,甚至不包括基礎方法。
var o = Object.create(null); //o不會繼承任何屬性和方法,空空的。
如果想建立一個普通的空對象,需要傳入Object.prototype
var o = Object.create(Object.prototype); //o相當于{}
對象屬性的擷取和設定
可以通過點(.)或方括号([])運算符來擷取和設定屬性的值。
var author = book.author;
var title = book["main title"];
在JavaScript中能用 . 連接配接的都可以用 []連接配接。有很多 . 運算符不能用的時候,就需要用[]代替。
1、在屬性名可變的情況下用[]
function getAttr (obj, attr) {
console.log(obj[attr])
}
2、屬性名有空格或者連字元等時用[]
var title = book["main title"];
删除屬性
delete運算符可以删除對象的屬性。
delete隻是斷開屬性和宿主對象的聯系,而不會去操作屬性中的屬性,如果删除的屬性是個對象,那麼這個對象的引用還是存在的。
var a = {b:{c:1}};
var b = a.b;
console.log(b.c); // 1
console.log(a.b); // {c:1}
delete a.b;
console.log(b.c); // 1
console.log(a.b); //undefined
delete隻能删除自有屬性,不能删除繼承屬性。
傳回值
傳回值為true
當delete表達式删除成功或沒有任何副作用(比如删除不存在的屬性),或者delete後不是一個屬性通路表達式,delete會傳回 true ;
var a = {b:{c:1}};
console.log(delete a.b);
console.log(delete a.b);
console.log(delete a.toString);
console.log(delete 1);
以上都會列印true
傳回值為false
delete不能删除那些可配置性為false的屬性,例如某些内置對象的屬性是不可配置的,通過變量聲明和函數聲明建立的全局對象的屬性。
var a = {};
Object.defineProperty(a,'b',{
value:1,
configurable: false // 設定為不可配置
})
console.log(delete a.b)
console.log(delete Object.prototype)
var x = 1;
console.log(delete this.x);
console.log(delete x)
以上列印都為false
檢測屬性
in 運算符
in 運算符的左側是屬性名(字元串),右側是對象。如果對象的自有屬性或繼承屬性中包含這個屬性則傳回true。
var a = {b:1};
console.log('a' in window); // true 聲明的全局變量'a'是window的屬性
console.log('b' in a); // true 'b'是a的屬性
console.log('toString' in a); // true a繼承了toString屬性
console.log('c' in a); // false 'c'不是a的屬性
跟in運算符類似的,還可以用”!==”判斷一個屬性是否是undefined,但是有一種場景隻能使用in運算符,in可以區分不存在的屬性和存在但值為undefined的屬性。
var a = {b:undefined};
console.log(a.b !== undefined); //false
console.log(a.c !== undefined); //false
console.log('b' in a); //true
console.log('c' in a); //false
hasOwnProperty
對象的hasOwnProperty()方法用來檢測給定的名字是否是對象的自有屬性。對于繼承屬性它将傳回false
var a = {b:1};
console.log(a.hasOwnProperty('b')); //true
console.log(a.hasOwnProperty('c')); //false
console.log(a.hasOwnProperty('toString')); //false toString是繼承屬性
propertyIsEnumerable
對象的propertyIsEnumerable()方法隻有檢測到是自身屬性(不包括繼承的屬性)且這個屬性的可枚舉性為true時它才傳回true。
var a = {b:1};
console.log(a.propertyIsEnumerable('b'));
console.log(a.propertyIsEnumerable('toString'));
包裝對象
當使用原始類型的值(string、number、boolean),在調用對應屬性和方法的時候,内部會自動轉成對應的對象。隐式建立的這個對象,就成為包裝對象。
基本類型都有自己對應的包裝對象 : String Number Boolean
包裝對象的特點
隐式建立對象後,可以調用對應的屬性和方法
使用後,立馬銷毀,是以不能給原始類型的值添加屬性和方法
其過程舉例:str.substring - > new String(1234) - > 找到String的substring -> 将new String銷毀
對象方法和屬性的彙總
Object靜态方法
- Object.assign()
- Object.create()
- Object.defineProperty()
- Object.defineProperties()
- Object.entries()
- Object.preventExtensions()
- Object.isExtensible()
- Object.seal()
- Object.isSealed()
- Object.freeze()
- Object.isFrozen()
- Object.keys()
- Object.values()
- Object.getPrototypeOf()
- Object.getOwnPropertyNames()
- Object.getOwnPropertyDescriptor()
- Object.getOwnPropertyDescriptors()
Object的執行個體方法(定義在Object.prototype上的)
- Object.prototype.hasOwnProperty()
- Object.prototype.isPrototypeOf()
- Object.prototype.propertyIsEnumerable()
- Object.prototype.toString()
- Object.prototype.valueOf()
面向對象
編碼思想
兩種程式設計方式:
(1)、面向過程
(2)、面向對象
兩者的差別:
面向過程:關注實作過程和每一步的實作細節。
面向對象:關注特征和功能。
面向對象程式設計
通俗點,用對象的思想寫代碼就是面向對象程式設計。
基本特征
1、抽象:抓住核心問題(簡單了解為抽出像的部分;将相同或表現與問題相關特征的内容提取出來。)
其核心:抽出、抽離,将相同的部分(可能會維護、會疊代、會擴充)的代碼抽離出來形成一類
2、封裝:就是将類的屬性包裝起來,不讓外界輕易知道它内部的具體實作;隻提供對外接口以供調用
3、繼承:從已有對象上繼承出新的對象
4、多态:一個對象的不同形态
面向對象的好處
1、代碼的層次結構更清晰
2、更容易複用
3、更容易維護
4、更容易擴充
面向對象相關的屬性和概念
__proto__
屬性原型鍊,執行個體對象與原型之間的連接配接,叫做原型鍊。
對象身上隻有 proto 構造函數身上有prototype也有 proto
constructor
傳回建立執行個體對象的構造函數的引用,每個原型都會自動添加constructor屬性,for..in..周遊原型是找不到這個屬性的。
var a = new A();
console.log(a.constructor == A) //true
hasOwnProperty
可以用來判斷某屬性是不是這個構造函數的内部屬性(不包括繼承的)
文法: obj.hasOwnProperty(prop) 傳回Boolean
function A (){
this.b = 1;
}
var a = new A();
console.log(a.hasOwnProperty('b')); //列印true
console.log(a.hasOwnProperty('toString')); //toString是繼承屬性 列印 false
console.log(a.hasOwnProperty('hasOwnProperty')); //同上,列印false
nstanceof
二進制運算符,用來檢測一個對象在其原型鍊中是否存在一個構造函數的 prototype 屬性。
文法: object instanceof constructor 即檢測 constructor.prototype 是否存在于參數 object 的原型鍊上。
// 定義構造函數
function C(){}
function D(){}
var o = new C();
o instanceof C; // true,因為 Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,因為 D.prototype不在o的原型鍊上
o instanceof Object; // true,因為Object.prototype.isPrototypeOf(o)傳回true
C.prototype instanceof Object // true,同上
toString
傳回一個表示該對象的字元串
作用:
1、進行數字之間的進制轉換
例如:var num = 255;
alert( num.toString(16) ); //結果就是'ff'
2、利用toString做類型的判斷
例如:var arr = [];
alert( Object.prototype.toString.call(arr) == '[object Array]' ); 彈出true
Object.prototype.toString.call() 得到是類似于'[object Array]' '[object Object]'
面向對象的寫法曆程
1、原始模式
假如我們有一個對象是狗的原型,這個原型有“名字”和“顔色”兩個屬性。
var Dog = {
name: ”,
color: ”
}
根據這個原型對象,我們要生成一個執行個體對象如下
var hashiqi = {}; //建立空對象,之後根據原型對象的相應屬性指派
hashiqi.name = 'hashiqi';
hashiqi.color = 'blackandwhite';
缺點:
1、如果要生成多個執行個體對象,要重複寫多次。
2、執行個體和原型之間沒有聯系。
2、工廠模式
上面原始模式有一個缺點是要很麻煩的寫很多重複的代碼,我們可以寫一個函數來解決代碼重複的問題。
function Dog(name, color) {
var obj = {};
obj.name = name;
obj.color = color;
return obj;
}
var hashiqi = Dog('hashiqi', 'blackandwhite');
var jinmao = Dog('jinmao', 'yellow');
這種方式隻是解決了代碼重複的問題,但是生成的執行個體跟原型還是沒有聯系,而且hashiqi和jinmao也沒有聯系,不能反映出他們是同一個原型對象的執行個體。
3、構造函數模式
用來建立對象的函數,叫做構造函數,其實就是一個普通函數,但是預設函數名首字母大寫,對構造函數使用new運算符,就能生成執行個體,并且this變量會綁定在執行個體對象上。
function Dog(name, color) {
this.name = name;
this.color = color;
}
var hashiqi = new Dog('hashiqi', 'blackandwhite');
var jinmao = new Dog('jinmao', 'yellow');
console.log(hashiqi.name); //hashiqi
console.log(jinmao.name); //jinmao
hasiqi 和 jinmao有一個共同的構造函數 hashiqi.constructor === jinmao.constructor 是true
有以下幾種方法可以驗證原型對象與執行個體對象的關系:
hashiqi instanceof Dog; // true
Object.getPrototypeOf(hashiqi) === Dog.prototype // true
Dog.prototype.isPrototypeOf(hashiqi) // true
構造函數解決了代碼重複和執行個體與原型之間的聯系,但是存在一個浪費記憶體的問題。比如遠行對象有一些不變的屬性和通用的方法,這樣沒生成一個執行個體,都必須為重複的東西多占一些記憶體。
擴充
我們可以嘗試實作new運算符的邏輯如下:
function New(func) {
var obj = {};
//判斷構造函數是否存在原型,如果有執行個體的__proto__屬性就指向構造函數的prototype
if(func.prototype !== undefined) {
obj.__proto__ = func.prototype;
}
// 模拟出構造函數内部this指向執行個體的過程,注意,我們會拿到構造函數的傳回值
var res = func.apply(obj, Array.from(arguments).slice(1));
// 正常構造函數是不需要顯式聲明傳回值的,預設的傳回值是生成的執行個體,但是一旦在構造函數中return 一個不是對象或者函數,就會改變構造函數的預設的傳回值,其他的類型是不變的
if(typeof res === 'object' && res !== null || typeof res === 'function') {
return res;
}
return obj;
}
var taidi = New(Dog, 'taidi', 'gray');
正常的構造函數是不需要自己寫return 的,如果寫了,當return的時候,如果是後面為簡單類型,那麼傳回值還是構造函數生成的執行個體。如果return為對象類型或者函數,那麼傳回的就是return後面的這個對象或者函數。
4、prototype模式
每一個構造函數都有 prototype 屬性,這個屬性指向的是一個對象,這個對象的所有屬性和方法,都會被構造函數的執行個體繼承。
基于這個屬性,我們就可以有選擇性的将一些通用的屬性和方法定義到 prototype 上,每一個通過 new 生成的執行個體,都會有一個 proto 屬性指向構造函數的原型即 prototype ,這樣我們定義到構造函數原型對象的屬性和方法,就會被每一個執行個體通路到,進而變成公用的屬性和方法。
function Dog(name, color) {
this.name = name;
this.color = color;
}
Dog.prototype.say = function () {
console.log("汪汪");
}
var hashiqi = new Dog('hashiqi', 'blackandwhite');
var jinmao = new Dog('jinmao', 'yellow');
hashiqi.say(); // 汪汪
jinmao.say(); // 汪汪
console.log(hashiqi.say === jinmao.say); // true
注意:當執行個體對象和原型對象有相同的屬性或者方法時,會優先通路執行個體對象的屬性或方法。
面向對象的繼承
1、構造函數内部的屬性和方法繼承
使用call或apply方法,将父對象的構造函數綁定在子對象上。
//父類
function Animal() {
this.species = '動物';
}
//子類
function Dog(name, color) {
Animal.call(this);
this.name = name;
this.color = color;
}
var hashiqi = new Dog('hashiqi', 'blackandwhite');
console.log(hashiqi.species); //動物
2、prototype相關的繼承
子類的prototype指向父類生成執行個體
function Animal() {};
Animal.prototype.species = '動物';
function Dog(name, color) {
this.name = name;
this.color = color;
}
Dog.prototype = new Animal();
//隻要是prototype被完全覆寫,都得重寫constructor。
Dog.prototype.constructor = Dog;
var hashiqi = new Dog('hashiqi', 'blackandwhite');
缺點: 每一次繼承都得生成一個父類執行個體,比較占記憶體。
利用空對象作為中介
function Animal() {}
Animal.prototype.species = '動物';
function Dog(name, color) {
this.name = name;
this.color = color;
}
//Middle生成的是空執行個體(除了__proto__),幾乎不占記憶體
function Middle() {}
Middle.prototype = Animal.prototype;
Dog.prototype = new Middle();
Dog.prototype.constructor = Dog;
var hashiqi = new Dog('hashiqi', 'blackandwhite');
console.log(hashiqi.species);
幾個月前在 CSDN 面試的時候,我說了這種繼承方式,面試官就糾結這樣修改子類的prototype不會影響父類麼?是真的不會影響的,因為子類的prototype是指向Middle構造函數生成的執行個體,如果真的有心要改,得Dog.prototype.proto這麼着來改。
function Animal() {}
Animal.prototype.species = '動物';
function Dog(name, color) {
this.name = name;
this.color = color;
}
Dog.prototype = Object.create(Animal.prototype,{
constructor: {
value: Dog
}
})
var hashiqi = new Dog('hashiqi','blackandwhite');
console.log(hashiqi.species); //動物
3、拷貝繼承
淺拷貝
function Animal() {}
Animal.prototype.species = '動物';
function Dog(name, color) {
this.name = name;
this.color = color;
}
function extend(child, parent) {
var c = child.prototype;
var p = parent.prototype;
for(key in p) {
c[key] = p[key]
}
}
extend(Dog, Animal);
var hashiqi = new Dog('hashiqi', 'blackandwhite');
console.log(hashiqi.species) // 動物
深拷貝
function deepCopy(parent, child) {
var child = child || {};
for(key in parent) {
if(typeof parent[key] === 'object') {
child[key] = parent[key].constructor === Array?[]:{};
deepCopy(parent[key],child[key])
} else {
child[key] = parent[key];
}
}
return child;
}
ES6的面向對象
上面所說的是JavaScript語言的傳統方法,通過構造函數,定義并生成新的對象。
ES6中提供了更接近傳統語言的寫法,引入了Class(類)的概念,通過class關鍵字,可以定義類。
文法
ES6的類完全可以看成是構造函數的另外一種寫法。
var method = 'say';
class Dog {
constructor (name,color) {
this.name = name;
this.color = color;
}
//注意,兩個屬性之間跟對象不同,不要加逗号,并且類的屬性名可以使用變量或者表達式,如下
[method] () {
console.log('汪汪');
}
}
console.log(typeof Dog); // function 類的資料類型就是函數
console.log(Dog === Dog.prototype.constructor); // true 類本身就是構造函數
既然是構造函數,是以在使用的時候,也是直接對類使用new指令,跟構造函數的用法完全一緻。
var hashiqi = new Dog('hashiqi', 'blackandwhite');
console.log(hashiqi.color); // blackandwhite
//上面采用表達式聲明的類的屬性可以用一下兩種方式調用
hashiqi[method](); // 汪汪
hashiqi.say(); // 汪汪
1、先聲明定義類,再建立執行個體,否則會報錯
class 不存在變量提升,這一點與ES5的構造函數完全不同
new Dog('hashiqi','blackandwhite')
class Dog {
constructor (name,color) {
this.name = name;
this.color = color;
}
}
//Uncaught ReferenceError: Dog is not defined
//上面代碼,Dog類使用在前,定義在後,因為ES6不會把類的聲明提升到代碼頭部,是以報錯Dog沒有定義。
2、必須使用new關鍵字來建立類的執行個體對象
類的構造函數,不使用new是沒法調用的,會報錯。 這是它跟普通構造函數的一個主要差別,後者不用new也可以執行。
class Dog {
constructor (name,color) {
this.name = name;
this.color = color;
}
}
Dog(); // Uncaught TypeError: Class constructor Dog cannot be invoked without 'new'
3、定義“類”的方法的時候,前面不需要加上function這個關鍵字,直接把函數定義放進去了就可以了。并且,方法之間不需要逗号分隔,加了會報錯。
屬性概念
constructor 構造函數
構造方法constructor是一個類必須要有的方法,預設傳回執行個體對象;建立類的執行個體對象的時候,會調用此方法來初始化執行個體對象。如果你沒有編寫constructor方法,執行的時候也會被加上一個預設的空的constructor方法。
constructor方法是必須的,也是唯一的,一個類體不能含有多個constructor構造方法。
class Dog {
constructor (name,color) {
this.name = name;
this.color = color;
}
//定義了兩個constructor,是以會報錯
constructor () {
}
}
new Dog('hashiqi', 'blackandwhite')
//Uncaught SyntaxError: A class may only have one constructor
Class表達式
與函數一樣,類可以使用表達式的形式定義。
const Hashiqi = class Dog {
constructor (name,color) {
this.name = name;
this.color = color;
}
getName () {
//此處的Dog就是Dog構造函數,在表達式形式中,隻能在構造函數内部使用
console.log(Dog.name);
}
}
var hashiqi = new Hashiqi('hashiqi', 'blackandwhite'); // 真正的類名是Hashiqi
var jinmao = new Dog('jinmao', 'yellow'); // 會報錯,Dog沒有定義
通常我們的表達式會寫成如下,省略掉類後面的名稱
const Hashiqi = class {
constructor (name,color) {
this.name = name;
this.color = color;
}
}
var hashiqi = new Hashiqi('hashiqi', 'blackandwhite');
執行個體方法和靜态方法
執行個體化後的對象才可以調用的方法叫做執行個體方法。
直接使用類名即可通路的方法,稱之為“靜态方法”
類相當于執行個體的原型,所有在類中定義的方法,都會被執行個體繼承。如果在一個方法前,加上static關鍵字,就表示該方法不會被執行個體繼承,而是直接通過類來調用,這就稱為“靜态方法”。
class Dog {
constructor (name,color) {
this.name = name;
this.color = color;
}
static say () {
console.log('汪汪');
}
}
Dog.say(); //汪汪
靜态方法和執行個體方法不同的是:靜态方法的定義需要使用static關鍵字來辨別,而執行個體方法不需要;此外,靜态方法通過類名來的調用,而執行個體方法通過執行個體對象來調用。
類的繼承
extends
類之間可以通過extends關鍵字實作繼承,這比ES5的通過修改原型鍊實作繼承,要清晰和友善很多。
class Dog extends Animal{}
extends的繼承目标
extends關鍵字後面可以跟多種類型的值,有三種特殊情況
1、子類繼承Object類
class A extends Object {}
console.log(A.__proto__ === Object) //true
console.log(A.prototype.__proto__ == Object.prototype) //true
//這種情況下,A其實就是構造函數Object的複制,A的執行個體就是Object的執行個體。
2、不存在繼承
class A {}
console.log(A.__proto__ === Function.prototype) // true
console.log(A.prototype.__proto__ === Object.prototype) // true
//這種情況下,A作為一個基類(即不存在任何繼承),就是一個普通函數,是以直接繼承Funciton.prototype。
//但是,A調用後傳回一個空對象(即Object執行個體),是以A.prototype.__proto__指向構造函數(Object)的prototype屬性。
3、子類繼承null
class A extends null {}
console.log(A.__proto__ === Function.prototype) //true
console.log(A.prototype) //隻有一個constructor屬性,沒有__proto__屬性
這種情況與第二種情況非常像。A也是一個普通函數,是以直接繼承Funciton.prototype。
但是,A調用後傳回的對象不繼承任何方法,是以沒有__proto__這屬性
super
uper這個關鍵字,既可以當作函數使用,也可以當作對象使用。
1、super作為函數調用時,代表父類的構造函數。作為函數時,super()隻能用在子類的構造函數之中,用在其他地方就會報錯。
2、super作為對象時,在普通方法中,指向父類的原型對象;在靜态方法中,指向父類。
class Animal {
constructor (name) {
this.name = name;
this.species = '動物';
}
say (){
return this.species;
}
}
class Dog extends Animal{
constructor (name, color) {
// 隻要是自己在子類中定義constructor,必須調用super方法,否則建立執行個體會報錯
//super作為函數調用,隻能用在子類的constructor中
super(name);
this.color = color;
}
getInfo () {
//普通方法中,super指向父類的原型對象
console.log(super.say()+': '+this.name +','+this.color);
}
}
var hashiqi = new Dog('hashiqi', 'blackandwhite');
hashiqi.getInfo() //動物:hashiqi,balckandwhite
1、子類必須在constructor方法中調用super方法,否則建立執行個體時會報錯。這是因為子類沒有自己的this對象,而是繼承父類的this對象,然後對其進行加工。如果不調用super方法,子類就得不到this對象。
2、在子類的普通方法中,由于super指向父類的原型對象,是以定義在父類執行個體上的方法或屬性,是無法通過super調用的。
3、使用super的時候,必須顯式指定是作為函數、還是作為對象使用,否則會報錯。
原文位址:
https://segmentfault.com/a/1190000012728341