第一次覺得“原型鏈和繼承”看着是那麼那麼順眼...

目錄:
1、原型&原型鏈注意
2、js中繼承的六種方式
3、new關鍵字
4、instance of
5、Object.create
6、複製實現繼承:淺拷貝、深拷貝
7、繼承理解DOM
8、創造對象的四種方式

1、原型&原型鏈注意

提到原型說對象(目錄8),每一個對象都有一個__proto__隱式原型屬性,指向構造改對象的構造函數的原型。
而函數比較特殊,它具備prototype這樣一個指針屬性,指向包含所有實例共享的屬性和方法的對象,稱爲原型對象;原型對象有一個constructor屬性,指向該函數。
因此具備:Object.\_\_proto\_\_ === Function.prototype;Function.\_\_proto\_\_ === Function.prototype。Object是對象的構造函數,對象的構造函數均指向“Function.prototype”,因爲Function本身是一個構造函數,爲不產生混亂,就將兩個聯繫到一起。
幾乎所有 JavaScript 中的對象都是位於原型鏈頂端的 Object 的實例。
function Person(){}
let p1 === new Person()
let obj === {}

p1.__proto__ === Person.prototype

Person.__proto__ === Function.prototype
Person.prototype
Person.prototype.__proto__ === Object.prototype
Person.prototype.constructor === Person

Function.__proto__ === Function.prototype
Function.prototype
Function.prototype.__proto__ === Object.prototype
Function.prototype.constructor === Function

obj.__proto__ === Object.prototype

Object.__proto__ === Function.prototype
Object.prototype
Object.prototype.__proto__ === null
Object.prototype.constructor === Object

JavaScript 並沒有其他基於類的語言所定義的“方法”。在 JavaScript 裏,任何函數都可以添加到對象上作爲對象的屬性。函數的繼承與其他的屬性繼承沒有差別,包括上面的“屬性遮蔽”(這種情況相當於其他語言的方法重寫)。
當繼承的函數被調用時,this 指向的是當前繼承的對象,而不是繼承的函數所在的原型對象。

2、js中繼承的六種方式

1、原型鏈繼承

特點:通過原型鏈繼承,引用類型會因爲引用的特點變成公用屬性;且無法在繼承的時候傳遞參數;可以使用instanceof、isPropertyOf檢測出來

function Company(){
    this.address = '深圳總部';
    this.projects = ['games']
}
Company.prototype.showAddress = function(){
    console.log("address爲" + this.address);
}

function SonCompany(address){
    this.address = address;
}

SonCompany.prototype = new Company();
// 將SonCompany.prototype.__proto__由指向Object.prototype轉爲指向Company.prototype,繼而SonCompany.prototype.__proto__.__proto__ === Object.prototype

var sonC1 = new SonCompany('廣州分部');
var sonC2 = new SonCompany('上海分部');
sonC1.projects.push('movies');
console.log(sonC2.projects); // ["games", "movies"]
console.log(sonC1);
/*
SonCompany 
    address: "廣州分部"
    __proto__: Company
        address: "深圳總部"
        projects: (2) ["games", "movies"]
        __proto__:
            showAddress: ƒ ()
            constructor: ƒ Company()
            __proto__: Object

而沒有使用prototype繼承的時候,sonC1爲:
SonCompany {address: "廣州分部"}
    address: "廣州分部"
    __proto__:
        constructor: ƒ SonCompany(address)
        __proto__: Object
*/

/*
此時一般還是需要將sonCompany的constructor換回來的,即應該在SonCompany.prototype = new Company();之後操作SonCompany.prototype.constructor = SonCompany;
這樣操作之後,sonC1打印出來如下:
SonCompany {address: "廣州分部"}
    address: "廣州分部"
    __proto__: Company
        address: "深圳總部"
        constructor: ƒ SonCompany(address)
        projects: ["games"]
        __proto__:
            showAddress: ƒ ()
            constructor: ƒ Company()
            __proto__: Object
*/

2、構造函數繼承

思想:在子類型構造函數的內部call、apply調用父類構造函數
特點:解決了原型鏈“引用類型私有屬性變公有”、“不可傳遞參數”兩個問題;
但是沒有使用new生成一個父類實例的時候,方法寫在prototype上無法使用,所有方法需要在函數中定義。這樣的話就導致無法複用,每次構建實例的時候都會在每一個實例中保留方法函數,造成內容浪費,無法同步更新。寫在prototype上,就只有一份,更新可以同步更新。

function Company (address) {
  this.address = address;
  this.projects = ['games'];
  this.getAddress = function () {
    return this.address;
  }
}

Company.prototype.showAddress = function(){ console.log(this.address) }

// 子類
function SonCompany (address) {
  // 繼承了Company,同時還傳遞了參數
  Company.call(this, address);
  // 實例屬性
  this.staff = 20;
}

var com1 = new Company('深圳總部');
com1.getAddress(); // 深圳總部
com1.showAddress(); // 深圳總部
var sonC1 = new SonCompany('廣州分部');
sonC1.getAddress(); // 廣州分部
sonC1.projects.push('movies');
var sonC2 = new SonCompany('上海分部');
console.log(sonC2.projects) // ["games"]
sonC2.getAddress(); // 上海分部
sonC2.showAddress(); // Uncaught TypeError: sonC2.showAddress is not a function

/*
sonC1的打印: getAddress成爲sonC1的一個函數
SonCompany
    address: "sonC1Address"
    staff: 20
    getAddress: ƒ ()
    projects: (2) ["games", "movies"]
    __proto__:
        constructor: ƒ SonCompany(address)
        __proto__: Object

com1的打印:可以查到showName()
Company
    address: "SuperAddress"
    getAddress: ƒ ()
    projects: ["games"]
    __proto__:
        showAddress: ƒ ()
        constructor: ƒ Company(address)
        __proto__: Object
*/

3、組合繼承(1、2組合)

思想:將上述兩種結合,發揮各自優勢;是目前廣泛使用的繼承方式;但是對父類構造函數調用了兩次;

function Company (address, staff) {
  this.address = address;
  this.staffs = staff;
  this.projects = ['games'];
  this.getAddress = function () {
    return this.address;
  }
}

Company.prototype.showAddress = function(){ console.log(this.address) }

// 子類
function SonCompany (address, staff) {
  // 繼承了Company,同時還傳遞了參數
  Company.call(this, address);
  // 實例屬性
  this.staffs = staff;
}
SonCompany.prototype = new Company();
// 將SonCompany.prototype.__proto__由指向Object.prototype轉爲指向Company.prototype,繼而SonCompany.prototype.__proto__.__proto__ === Object.prototype
SonCompany.prototype.constructor = SonCompany;
// 將構造函數轉回來,因爲上一步操作之後:SonCompany.prototype.constructor === Company;轉之後即可在SonCompany的prototype上正常操作 
SonCompany.prototype.getStaff = function(){
    return this.staffs
}

var com1 = new Company('深圳總部', 100);
com1.getAddress(); // 深圳總部
com1.showAddress(); // 深圳總部
var sonC1 = new SonCompany('廣州分部', 20);
sonC1.getAddress(); // 廣州分部
sonC1.projects.push('movies'); // ['games', 'movies']
var sonC2 = new SonCompany('上海分部', 40);
console.log(sonC2.projects) // ['games']
sonC2.getAddress(); // 上海分部
sonC2.showAddress(); // 上海分部
sonC2.getStaff(); // 40

/*
SonCompany
    address: "廣州分部"
    getAddress: ƒ ()
    projects: ["games"]
    staffs: 20
    __proto__: Company
        address: undefined
        constructor: ƒ SonCompany(address, staff)
        getAddress: ƒ ()
        getStaff: ƒ ()
        projects: ["games"]
        staffs: undefined
        __proto__:
            showAddress: ƒ ()
            constructor: ƒ Company(address, staff)
            __proto__: Object

*/

4、原型式繼承

思想:不適用嚴格意義上的構造函數,藉助原型基於已有對象創建新對象,同時不必創建自定義類型;
一般是僅僅模擬一個對象的時候使用; es5新增了一個方法Object.create(prototype, descripter)接收兩個參數:
prototype(必選),用作新對象的原型對象
descripter(可選),爲新對象定義額外屬性的對象
在傳入一個參數的情況下,Object.create()與前面寫的object()方法的行爲相同。
使用一個函數對傳入其中的對象執行一次淺複製,複製到的副本還需要進一步改造;引用類型屬性還是會被共享

function objectCopy(o){
    function F(){} // 創建一個臨時構造函數
    F.prototype = o; // 將傳入的對象作爲構造函數的原型
    return new F(); // 返回臨時類型的新實例
}
var company = {
    address: "深圳分部",
    projects: ['games'],
    getAddress: function(){
        console.log(this.address);
    }
}
var c1 = objectCopy(company);
var c2 = objectCopy(company);
c1.projects.push('movies');
console.log(c2.projects); // ["games", "movies"]
c1.getAddress(); // "深圳分部"

5、寄生式繼承

思想:創建一個僅用於封裝繼承過程的函數,改函數內部以某種形式來做增強對象,最後返回對象。

function objectCopy(o){
    function F(){}
    F.prototype = o;
    return new F();
}

function createAnother (original) {
  var clone = objectCopy(original) // 通過調用函數創建一個新對象
  clone.sayHi = function () { // 以某種方式來增強這個對象
    alert("hi")
  }
  return clone // 返回這個對象
}

var company = {
    address: "深圳分部",
    projects: ['games'],
    getAddress: function(){
        console.log(this.address);
    }
}
var c1 = createAnother(company);
var c2 = createAnother(company);
c1.projects.push('movies');
console.log(c2.projects); // ["games", "movies"]
c1.sayHi(); // hi
/*
c1打印
F {sayHi: ƒ}
    sayHi: ƒ ()
    __proto__:
        address: "深圳分部"
        getAddress: ƒ ()
        projects: (2) ["games", "movies"]
        __proto__: Object
*/

6、組合寄生式繼承(3、5)

思想:使用構造函數繼承屬性,原型鏈混成形式繼承方法;只調用一次構造函數
被認爲最理想的繼承方式;

function objectCopy(o){
    function F(){}
    F.prototype = o;
    return new F();
}

function inheritPrototype(s, f){
    var prototype = objectCopy(f.prototype); // 創建對象
    prototype.constructor = s; // 增強對象
    s.prototype = prototype; // 指定對象
}

function Company (address, staff) {
  this.address = address;
  this.staffs = staff;
  this.projects = ['games'];
  this.getAddress = function () {
    return this.address;
  }
}

Company.prototype.showAddress = function(){ console.log(this.address) }

// 子類
function SonCompany (address, staff) {
  // 繼承了Company,同時還傳遞了參數
  Company.call(this, address);
  // 實例屬性
  this.staffs = staff;
}

inheritPrototype(SonCompany, Company); // 繼承

SonCompany.prototype.getStaff = function(){
    return this.staffs
}

var com1 = new Company('深圳總部', 100);
com1.getAddress();
com1.showAddress();
var sonC1 = new SonCompany('廣州分部', 20);
sonC1.getAddress();
sonC1.projects.push('movies');
var sonC2 = new SonCompany('上海分部', 40);
console.log(sonC2.projects)
sonC2.getAddress();
sonC2.showAddress();
sonC2.getStaff();

/*
sonC1打印如下
SonCompany
    address: "廣州分部"
    getAddress: ƒ ()
    projects: (2) ["games", "movies"]
    staffs: 20
    __proto__: Company
        constructor: ƒ SonCompany(address, staff)
        getStaff: ƒ ()
        __proto__:
            showAddress: ƒ ()
            constructor: ƒ Company(address, staff)
            __proto__: Object
*/

7、es6的class實現繼承

底層使用的是組合寄生式繼承;寫法簡單易懂,如下:

class Company{
    constructor(address){
        this.address = address;
        this.projects = ['games']
    }
    getAddress(){
        console.log(this.address)
    }
}

class SonCompany extends Company{
    constructor(address, staffs){
        super(address) // 繼承父類實例屬性和prototype上的方法
        this.staffs = staffs
    }
    getStaffs(){
        console.log(this.staffs)
    }
}

var sonC1 = new SonCompany('廣州分部', 40);
var sonC2 = new SonCompany('上海分部', 20);
sonC1.getAddress();  // 廣州分部
sonC1.getStaffs();  // 40
sonC1.projects.push('movies');
sonC2.getAddress(); // 上海分部
sonC2.getStaffs(); // 20
sonC2.projects; // ['games']

3、new關鍵字

new做了四件事er:

1、創建一個新對象
2、將__proto__屬性指向構造器函數的prototype
3、將this指向新創建對象,使用新創建的對象執行構造函數
4、返回新建對象

function _new(fn){
	return function(){
		let obj = {  // 創建新obj
			__proto__: fn.prototype  // 原型鏈
		}
		// 執行構造函數,傳遞參數
		fn.call(obj, ...arguments); // apply也是OK的,不過參數問題這樣方便;所以根據這個,fn中的this指的是new的對象
		// 返回obj
		return obj
	}
}

function A(a){
	this.a = a
}
var obj = _new(A)('aa');  // {a: 'aa}

函數科裏化: fun裏return一個fun

4、instance of

會根據原型鏈一直找下去

function instance_of(L, R) {//L 表示左表達式,R 表示右表達式
        var O = R.prototype;// 取 R 的顯示原型
        L = L.__proto__;// 取 L 的隱式原型
        while (true) { 
        if (L === null) //L是Object.prototype
            return false; 
        if (O === L)// 這裏重點:當 O 嚴格等於 L 時,返回 true 
            return true; 
        L = L.__proto__; 
    } 
}

5、Object.create

判斷第一個參數是否符合繼承的特點,即是否爲具有__proto__屬性,是否爲對象,函數也是對象;
判斷第二個參數是否符合條件,即不爲undefined;將對象賦值給第一個參數

var _create = function (proto, propertyObject = undefined) {
    // 先判斷proto是否符合條件
    if (typeof proto !== 'object' && typeof proto !== 'function') {
        throw new TypeError('Object prototype may only be an Object: ' + proto);
    }
    /*
    else if (proto === null) {
        throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
    }
    */

    if (propertyObject === null) {
        throw 'TypeError'
    } else {
        function Fn() { }
        Fn.prototype = proto
        const obj = new Fn()
        if (propertyObject !== undefined) {
            Object.defineProperties(obj, propertyObject)
        }
        if (proto === null) {
            // 創建一個沒有原型對象的對象,Object.create(null)
            obj.__proto__ = null
        }
        return obj
    }
}


var  proto = {name:'allen'};
var  p = _create(proto,{age:{value:21}});
p.hasOwnProperty('age');//true
p.hasOwnProperty('name');//false

Object.create(null) vs {}
前者就是一個單純的{},不具備properties的屬性;{} 相當於 Object.create(Object.ptototype),存在n.toString(),且可以用for…in循環

6、複製實現繼承:淺拷貝、深拷貝

7、繼承理解DOM

DOM上是具備原型鏈的;
文檔對象模型 (DOM) 將 web 頁面與到腳本或編程語言連接起來。通常是指 JavaScript,但將 HTML、SVG 或 XML 文檔建模爲對象並不是 JavaScript 語言的一部分。DOM模型用一個邏輯樹來表示一個文檔,樹的每個分支的終點都是一個節點(node),每個節點都包含着對象(objects)。DOM的方法(methods)讓你可以用特定方式操作這個樹,用這些方法你可以改變文檔的結構、樣式或者內容。節點可以關聯上事件處理器,一旦某一事件被觸發了,那些事件處理器就會被執行。

8、創建對象的4種方式

來看看使用不同方法來創建對象:
1.語法結構

var o = {a: 1};
// o ---> Object.prototype ---> null ; o 這個對象繼承了 Object.prototype 上面的所有屬性

var a = [1, 2, 3]
// a ---> Array.prototype ---> Object.prototype ---> null;數組都繼承於 Array.prototype 

function f() { return true}
// f ---> Function.prototype ---> Object.prototype ---> null;函數都繼承於 Function.prototype;(Function.prototype 中包含 call, bind等方法)

2.構造器(即普通函數),即使用new操作符

function testF(a){ this.a =  a }
testF.prototype = {
	funA: function(){
		console.log(this.a)
	}
}
var t = new testF('aaa');

3.使用Object.create創建對象(ES5方法),新對象的原型就是調用create時傳入的第一個參數

var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (繼承而來)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因爲d沒有繼承Object.prototype

4.使用class關鍵字(ES6引入的語法糖,雖然有class,extends的概念,單js還是基於原型)

class A { static geta(){ console.log( 'A' ); } }
class B extends A { static getb(){ console.log( 'B' ); } }
B.geta(); // A
B.getb(); // B 
要檢查對象是否具有自己定義的屬性,而不是其原型鏈上的某個屬性,則必須使用所有對象從 Object.prototype 繼承的 hasOwnProperty 方法。(在原型鏈上查找屬性比較耗時,對性能有副作用,這在性能要求苛刻的情況下很重要。另外,試圖訪問不存在的屬性時會遍歷整個原型鏈。)遍歷對象的屬性時,原型鏈上的每個可枚舉屬性都會被枚舉出來。Object.keys()也是跟 hasOwnProperty 一樣的效果;(注意:檢查屬性是否爲 undefined 是不能夠檢查其是否存在的。該屬性可能已存在,但其值恰好被設置成了 undefined。)
原型鏈是可擴展的,但是不建議隨意擴展【原生對象】的原型,這樣會影響到很多繼承原生對象的使用;

擴展原型鏈大概有一下4種:new、Object.create、Object.setPrototypeOf、proto 越靠後越要求版本等,還是建議new

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