個人向,對JS知識進行了查漏補缺,主要來源于《JS高級程序設計》和網上博客,本文內容主要包括以下:
- 對象
- 創建對象
- 繼承
一、對象
特性(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
做了什么呢?
- 創建一個新對象
- 把構造函數的作用域賦給新對象(this指向新對象)
- 執行構造函數代碼(給新對象添加屬性)
- 返回新對象
手動模擬一下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繼承的語法糖
- 不同點:
- ES6繼承中子類的構造函數的原型鏈指向父類的構造函數,ES5中使用的是構造函數復制,沒有原型鏈指向。
- 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__
}
}
}