JavaScript基礎複習(六) 對象,原型(鏈),繼承

最近在複習基礎知識,這次整理的內容是JS對象,原型,原型鏈,繼承~

本次知識的複習是基於《JavaScript高級程序設計》加上自己的理解,如果有什麼問題,望不吝賜教。

對象

什麼是對象: 無序屬性的集合,其屬性可以包含基本值,對象或者函數。 在js中就是 key-value 的鍵值對

對象的屬性類型

  • 數據屬性
  • 訪問器屬性

數據屬性

包含一個數據值的位置,可以讀取和寫入。

包含4個特性[Configurable, Enumerable, Value, Writable]Value 外,默認爲true。這裏表示的是 對象本身的屬性

如果使用 Object.defineProperty(obj, va, des), 定義新屬性或修改原有的屬性, 那麼這裏的 des,是個對象,包含對於{configurable: false || true, enumerable: false || true, value: undefined, writable: false || true}的設置, 除value外,默認爲 false。指的是 設置對象屬性時

使用

var obj = { a: 20 };
// 未設置屬性, 默認均爲 false
Object.defineProperty(obj, 'b', { value: 25 }) 
// 這是設置 屬性 後的
Object.defineProperty(obj, 'c', {
    configurable: true, // 允許修改或刪除
    writable: true, // 允許重寫
    value: 30,
    enumerable: true, // 允許可枚舉
})
console.log('學習對象 1', obj); // {a: 20, c: 30, b: 25}
// b在c的後面,且是置灰狀態,因爲b不可枚舉。
console.log(Object.keys(obj)); //  ['a', 'c']

訪問器屬性

不包含數據值,是getter和setter函數

包含4個特性[Configurable, Enumerable, Get, Set],Get, Set 默認值爲 undefined,,Configurable, Enumerable默認爲true

只能使用 Object.defineProperty(obj, va, des)來定義屬性, {configurable: false || true, enumerable: false || true, get: undefined, set: undefined} 默認爲false / undefined

使用

var obj1 = {}; 
var bValue;
Object.defineProperty(obj1,"newKey",{
    get:function (){ return bValue },
    set:function (value){ bValue = value },
    configurable: true,  // 允許修改或刪除
    enumerable: true,  // 允許可枚舉
});
console.log('學習對象2', obj1);
console.log(obj1.newKey);
console.log(Object.values(obj1));

總的來說,如果一個描述符不具有 value、writable、get 和 set中的任意一個鍵,那麼它將被認爲是一個數據描述符。如果一個描述符同時擁有 valuewritablegetset 鍵,則會產生一個異常。

爲對象一次定義多個屬性

使用方法:

Object.defineProperties(a, b) // a: 要添加或修改其屬性的對象 b: 要修改成什麼樣,與a對應,可以寫成 {c:, e, g, s,...}

使用


var obj2 = {};
var vv;
Object.defineProperties(obj2, {
    value1: {
        value: '設置多個值',
        // configurable: true,
        // writable: true,
        // enumerable: true,
    },
    newNumber: {
        get: function(){
            return vv
        },
        enumerable: true,
        set: function(v) {
           vv =  v
        },
        configurable: true,
    }
})
console.log('學習對象3', obj2);

還有兩個方法,獲取屬性的特性值

Object.getOwnPropertyDescriptor(obj, prop)

返回指定對象上一個自有屬性對應的屬性描述符。自有屬性指的是直接賦予該對象的屬性,不需要從原型鏈上進行查找的屬性

Object.getOwnPropertyDescriptors(obj)

用來獲取一個對象的所有自身屬性的描述符

console.log('學習對象4', Object.getOwnPropertyDescriptor(obj2, 'value1')); // 會拿到對應屬性的所有特性值,如沒有設置,就是默認值
console.log('學習對象5', Object.getOwnPropertyDescriptors(obj2)); // 拿到所有屬性的所有特性值,如沒有設置,就是默認值

創建對象

以下幾種方式: 1、直接創建 2、工廠模式 3、構造函數模式 4、原型模式 5、組合式使用構造函數和原型

直接創建

缺點: 代碼量太多,不利於複用
var personObj = {   
    name: 'deidei',
    jobNum: '007',
    eat: function(v) {
        return v;
    }
}
//  new Object({...})
console.log('學習對象直接創建', personObj, personObj.eat('meat'));

工廠模式

使用函數來封裝特定接口來創建對象

function createPerson (name, jobNum){
    var o = new Object();
    o.name = name;
    o.jobNum = jobNum;
    o.eat = function(v) {
        return v
    }
    return o;
}
console.log('學習對象工廠模式', createPerson('dei', '001'), createPerson('dei', '001').eat('rice'))

特點: 需要顯式創建,需要return

優點: 解決了創建多個相似對象的問題

缺點: 不能識別對象的類型

構造函數模式

本身也是函數,在使用中可以new,也可以當作普通函數使用


function CreateStaff (name, jobNum){
    this.name = name;
    this.jobNum = jobNum;
    this.eat = function (v){
        return v
    }
}
var staff2 = new CreateStaff('2', '002'); 
var staff3 = new CreateStaff('3', '003');

console.log('學習對象構造函數', staff2, staff2.eat('chicken'));
console.log('學習對象構造函數', staff3, staff3.eat('vegatable'));

// staff2 ,staff3 是 CreateStaff 的 實例,都有一個 constructor 的屬性,指向 CreateStaff
console.log(staff2.constructor == CreateStaff, staff2.constructor == staff3.constructor);  //true

特點:沒有顯式創建,不需要return,使用 this對象

優點: 將實例標識爲特定的類型

缺點: + 每次new一個對象,方法需要在構造函數上重新創建一遍,相當於 每次 都要new Function( return v )
+ 這樣每次創建的實例都是包含不同的Function實例,會導致不同的作用域鏈和標識符解析。但是沒有必要,因爲這兩個不同Function完成同樣的任務。

=> 引出 原型模式

第一次解決: 將方法放在 全局作用域,在構造函數中直接調用全局的函數 =》 不可取, 封裝性太弱,

第二次解決: 原型模式,原因:原型對象包含可以由特定類型的所有實例共享的屬性和方法。

原型模式

給原型對象上添加屬性和方法,可以讓對象實例共享


function CreateStaffNew() {

}
CreateStaffNew.prototype.name = '4';
CreateStaffNew.prototype.jobNum = '004';
CreateStaffNew.prototype.eat = function(v) { return v }
var staff4 = new CreateStaffNew(); 
var staff5 = new CreateStaffNew();
console.log('學習對象原型模式', staff4.eat('furit'), staff5.eat('fish'), staff4.eat == staff5.eat); // furit fish true
// 屬性和方法都是共享的。

原型對象 :任何函數,都有一個propotype屬性,這個屬性所指向的就是這個函數的原型對象。


// 原型詳解: 
// prototype: 任何函數,如構造器,普通函數等 都有 原型;這個原型也一定有個 constructor 屬性,指向構造函數本身。
console.log('學習對象原型模式 原型', CreateStaffNew.prototype.constructor == CreateStaffNew) // true
// __proto__  : 隱式原型,所有js對象都有,指向創造這個對象的 構造器 的 原型,即構造器函數也有,指向Function的原型,再指向 Object 的原型
console.log('學習對象原型模式 原型', staff4,  staff4.__proto__ == CreateStaffNew.prototype); // true

console.log('學習對象原型模式 原型', CreateStaffNew.prototype.isPrototypeOf(staff4)) // true   判斷實例對象與構造器之間 是否有 原型指向 的關係
console.log('學習對象原型模式 原型', Object.getPrototypeOf(staff5) == CreateStaffNew.prototype) // true
console.log('學習對象原型模式 原型', Object.getPrototypeOf(staff4).name)  // 4

區分這個屬性或方法來自原型鏈還是實例:

hasOwnProperty() 不會查找原型鏈 in 會查找原型鏈


console.log('學習對象原型模式 hasOwnProperty fasle', staff4.hasOwnProperty('name'));  //false
staff4.name = '測試hasOwnProperty'
console.log('學習對象原型模式 hasOwnProperty true', staff4.hasOwnProperty('name')); // true

確定屬性或方法是原型的還是實例的:

function hasPrototypeProperty(object, name) {
  return !object.hasOwnProperty(name) && (name in object);  // 實例上不存在 && 原型上存在   =》 在原型上
}

重寫原型對象

function ReCreateStaff(){

}
//  先創建實例
var re = new ReCreateStaff();
//   重寫原型對象,切斷了現有原型與任何之前已經存在的實例之間的關係,re引用的仍然是最初的原型
ReCreateStaff.prototype = {
    name: '9',
    jobNum: '009',
    sayName: function() { 
        return this.name
    }
};
var re1 = new ReCreateStaff();
// re.sayName(); // error  re.sayName is not a function
console.log(re1.sayName())  // 9

缺點: 屬性不可複用,都被寫到原型對象上了,也就是說只能用於一個對象的。這也就是爲何不單獨使用原型模式

組合式創建

使用構造函數定義不需要共享的屬性,使用原型模式定義需要共享的屬性和方法。


function CreateStaffComcat(name, jobNum){
    this.name = name;
    this.jobNum = jobNum;
}
CreateStaffComcat.prototype.eat = function(){
    return this.name
}

var staff6 = new CreateStaffComcat('6', '006');
console.log('學習對象組合模式', staff6, staff6.eat())

繼承

原型鏈

使用原型,讓一個引用類型繼承另一個引用類型的屬性和方法。

子構造函數的原型指向了父構造函數的實例對象,因此子構造函數的實例對象可以通過原型鏈找到父構造函數的原型方法和類屬性。


function CreateStaffPro(name) {
    this.name = name;
}
CreateStaffPro.prototype.say = function() { return this.name }

function Engineer() {

}
Engineer.prototype = new CreateStaffPro();  // 子構造函數的原型 指向 父構造函數的實例
var e1 = new Engineer('bbb111');  // 無法傳遞
// 子構造函數也就擁有 父構造函數的屬性和方法; 所有實例對象都可以共享父構造函數的原型方法
console.log('學習繼承原型鏈', e1.name, e1.say()); // undefined, undefined
// 但是並不是 Engineer 的屬性和方法,而是在執行的時候,需要再往上找一層,找到父構造函數。這樣實現不太好,如果找不到,就要一直向上找到原型鏈的末端。
// 所有函數的默認原型都是 Object()的實例
// 使用 instanceof ;   isPrototypeOf() 檢測 原型和實例 之間的關係
console.log('學習繼承原型鏈 instanceof', e1 instanceof Engineer, e1 instanceof CreateStaffPro); // true, true
console.log('學習繼承原型鏈 isPrototypeOf', Engineer.prototype.isPrototypeOf(e1), CreateStaffPro.prototype.isPrototypeOf(e1)); // true, true

缺點:

  • 引用類型值的原型屬性會被所有實例共享,如果繼承,實例屬性就會變成現在的原型屬性。
  • 創建子構造函數的實例時,無法向父構造函數中傳遞參數

=》 很少單獨使用原型鏈 來實現繼承

借用構造函數

其實就是在子類構造函數中,借調 父類的構造函數, 使用 call()/apply() 方法。


function CreateStaffGou(name) {
  this.name = name;
  this.hi = function() { return 'hi' }
}
CreateStaffGou.prototype.say = function() {return this.name}
function EngineerGou(name) {
    CreateStaffGou.call(this, name); // 只能拿到構造函數內部的屬性和方法,但拿不到 原型上定義的共享方法
}
var e2 = new EngineerGou('ccc111');
console.log('學習繼承構造函數',e2.name, e2.hi()); // ccc111 hi
// console.log(e2.say()) // error

優點:相比原型模式,有很大的一個優勢,就是可以,傳遞參數

缺點:

  • 實現了 屬性的傳遞,但是 方法都定義在子類的構造函數裏,無法複用
  • 在父構造函數中定義的方法,在子構造函數中,不可見

=》 很少單獨使用 構造函數 來實現繼承

組合繼承

借用構造函數繼承 不需要共享的屬性,使用 原型鏈來繼承 需要共享的屬性和方法。

function CreateStaffComcatExtend(name){
    this.name = name;
}
CreateStaffComcatExtend.prototype.sayName = function() { return this.name }
function EngineerExtend(name, jobNum) {
    // 繼承屬性
    CreateStaffComcatExtend.call(this, name)
    this.jobNum = jobNum;
}
// 繼承方法
EngineerExtend.prototype = new CreateStaffComcatExtend();
EngineerExtend.prototype.sayJobNum = function() { return this.jobNum }
var e3 = new EngineerExtend('10', '010');
console.log('學習繼承組合式',e3.name, e3.sayName(), e3.sayJobNum()) // 10 10 010

缺點:調用兩次父構造函數,一次是原型鏈繼承(繼承方法),一次是在借調構造函數繼承(繼承屬性) =》 產生兩個 name 屬性

原型式繼承

寄生式繼承

寄生組合繼承

解決 組合式繼承的問題

實現:借調構造函數繼承屬性(與組合繼承的構造函數一致),使用原型鏈的混成形式繼承方法(以父類的原型爲原型的構造函數實例化出來的對象作爲子類的原型(Object.create做的事情),完美避開了不必要的父類構造函數裏的東西。)

子類.原型 = new Object(父類.原型) 子類.原型.constructor = 子類

只調用一次 父構造函數


function CreateStaffComcatJiSheng(name) {
    this.name = name;
}
CreateStaffComcatJiSheng.prototype.sayName = function() { return this.name }
function EngineerExtendJiSheng(name, jobNum) {
    // 繼承屬性   不變化
    CreateStaffComcatJiSheng.call(this, name)
    this.jobNum = jobNum;
}
console.log('看看Object(f.prototype)是個什麼東西', CreateStaffComcatJiSheng.prototype, Object.create(CreateStaffComcatJiSheng.prototype))
// 替換 z.prototype = new f()
function inheritPrototype(f, z){
    // var prop = Object(f.prototype); //創建對象  使用父構造函數的副本
    z.prototype = Object(f.prototype);             //指定對象
    // prop.constructor = z;           //增強對象
}
inheritPrototype(CreateStaffComcatJiSheng, EngineerExtendJiSheng);
EngineerExtendJiSheng.prototype.sayJobNum = function() {return this.jobNum};
var e4 = new EngineerExtendJiSheng('11', '011');

console.log('學習繼承寄生組合式', e4.name, e4.sayName(), e4.sayJobNum()); // 11 11 011

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章