javascript的面向對象,原型鏈及繼承

面向對象

js是一門基於對象的語言。js中的一切皆對象;es6

console.log(Object.prototype.toString.call(123)) //[object Number]        
console.log(Object.prototype.toString.call('123')) //[object String]      
console.log(Object.prototype.toString.call(undefined)) //[object Undefined] 
console.log(Object.prototype.toString.call(true)) //[object Boolean]      
console.log(Object.prototype.toString.call({})) //[object Object]        
console.log(Object.prototype.toString.call([])) //[object Array]      
console.log(Object.prototype.toString.call(function(){})) //[object Function]

new function編程


var a = function () {};
    console.log(typeof a);//function
    
    var b = new function () {};
    console.log(typeof b);//object
    
    var c = new Function ();
    console.log(typeof c);//function
    
new function 是一個JavaScript中用戶自定義的對象

    var obj = function (name) {
        this.name = name;
    };
    var b = new obj('aaa')=o == Object.create(obj.prototype);;
    console.log(b.name);  
    // 建立一個以另外一個空對象爲原型,且擁有一個屬性p的對象
    o = Object.create({}, { p: { value: 42 } })
    
    // 省略了的屬性特性默認爲false,因此屬性p是不可寫,不可枚舉,不可配置的:
    o.p = 24
    o.p
    //42

私有變量和函數
在函數內部定義的變量和函數,叫局部(內部)變量和函數,若是不對外提供接口,外部是沒法訪問到的。數組

function Box(){
    var color = "blue"; //私有變量
    var fn = function(){} //私有函數
}
var obj = new Box();
    alert(obj.color); //彈出 undefined,訪問不到私有變量
    alert(obj.fn); //同上

靜態變量和函數
定義一個函數後加"."來添加的屬性和函數,該函數能夠訪問到,但實例訪問不到。瀏覽器

function Obj(){};

Obj.num = 72; //靜態變量
Obj.fn = function() { } //靜態函數
  
alert(Obj.num); //72
alert(typeof Obj.fn) //function

var t = new Obj();
alert(t.name); //undefined
alert(typeof t.fn); //undefined

實例變量和函數數據結構

function Box(){
    this.a=[]; //實例變量
    this.fn=function(){} //實例方法
}

var box1=new Box();
box1.a.push(1);
box1.fn={};
console.log(box1.a); //[1]
console.log(typeof box1.fn); //object

var box2=new Box();
console.log(box2.a); //[]
console.log(typeof box2.fn); //function

box1中修改了afn,而在box2中沒有改變,因爲數組和函數都是對象,是引用類型,這就說明box1中的屬性和方法與box2中的屬性與方法雖然同名但卻不是一個引用,而是對Box對象定義的屬性和方法的一個複製。app

ES5

構造函數

構造函數(constructor),其實就是一個普通函數,可是內部使用了this變量,對構造函數使用new運算符,就能生成實例,而且this變量會綁定在實例對象上函數

function Cat(name,color){
    this.name=name;
    this.color=color;
}
Cat.prototype.type=function(){};

var cat1=new Cat()

這時cat1會自動含有一個constructor屬性,指向它們的構造函數。this

alert(cat1.constructor==Cat);//true

alert(cat instanceof Cat);//true

js提供了一個instanceof運算符,用來檢驗cat1是不是Cat的實例對象。es5

原型對象與實例對象之間的關係。

構造函數(constructor):每new生成一個實例,就至關於在內存上又複製了一次
原型對象(prototype):而portotype,全部的實例都只指向一個內存地址,用於不變的屬性和方法spa

不論是構造函數內部仍是原型對象,裏面的this在沒有new以前都指向該構造函數Cat

Cat.prototype.constructor===Cat //true
alert(Cat.prototype.isPrototypeof(cat1)) //true

isPrototypeOf()用來判斷某個prototype對象和某個實例之間的關係

alert(cat1.hasOwnProperty("name")); // true
alert(cat1.hasOwnProperty("type")); // false

hasOwnProperty()用來判斷某個屬性究竟是本地屬性,仍是繼承自prototype對象的屬性。本地爲true

in運算符用來判斷,某個實例是否含有某個屬性,不論是不是本地屬性。還能夠用來遍歷某個對象的全部屬性。

alert("name" in cat1);//true
for(var i in cat1){alert("cat1["+i+"]="+cat1[i])}

繼承

構造函數綁定

function Cat(name,color){

    Animal.apply(this, arguments);//等因而把父類的實例屬性複製了一份給子類實例裝上了,佔內存

    this.name = name;
    this.color = color;

  }

  var cat1 = new Cat("大毛","黃色");
  alert(cat1.species); // 動物

利用prototype

Cat.prototype = new Animal(); //因爲prototype 引用類型指向同一個地址會影響其它實例,並且不能向父傳參
  Cat.prototype.constructor = Cat; //把prototype指向原來的構造函數

組合繼承

function Cat(){
        Animal.call(this);
    }
    Cat.prototype = new Animal(); //比較經常使用,佔內存
 

經過空對象

function extend(Child, Parent) {
    
    var F = function(){}; //利用一個空對象去轉接prototype,並且空對象幾乎不佔內存,不會影響父對象
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    
    Child.prototype.constructor = Child; //糾正回來它的構造函數指向
    Child.uber = Parent.prototype; //輔助屬性,能夠直接調用父的方法
  }

    function Animal(){ }
  Animal.prototype.species = "動物";

    function Cat(name,color){
        this.name=name
        this.color=color
    }

    extend(Cat,Animal);

  var cat1 = new Cat("大毛","黃色");
  alert(cat1.species); // 動物

拷貝繼承

function extend2(Child, Parent) {
    
    var p = Parent.prototype;
    var c = Child.prototype;
    
    for (var i in p) { //循環父的原型方法給到子
      c[i] = p[i];
    }

    c.uber = p;

  } 

    extend2(Cat, Animal);

  var cat1 = new Cat("大毛","黃色");
  alert(cat1.species); // 動物

原型和原型鏈

私有變量和函數
在函數內部定義的變量和函數,叫局部(內部)變量和函數,若是不對外提供接口,外部是沒法訪問到的。

function Box(){
    var color = "blue"; //私有變量
    var fn = function(){} //私有函數
}
var obj = new Box();
    alert(obj.color); //彈出 undefined,訪問不到私有變量
    alert(obj.fn); //同上

靜態變量和函數
定義一個函數後加"."來添加的屬性和函數,該函數能夠訪問到,但實例訪問不到。

function Obj(){};

Obj.num = 72; //靜態變量
Obj.fn = function() { } //靜態函數
  
alert(Obj.num); //72
alert(typeof Obj.fn) //function

var t = new Obj();
alert(t.name); //undefined
alert(typeof t.fn); //undefined

實例變量和函數

function Box(){
    this.a=[]; //實例變量
    this.fn=function(){} //實例方法
}

var box1=new Box();
box1.a.push(1);
box1.fn={};
console.log(box1.a); //[1]
console.log(typeof box1.fn); //object

var box2=new Box();
console.log(box2.a); //[]
console.log(typeof box2.fn); //function

box1中修改了afn,而在box2中沒有改變,因爲數組和函數都是對象,是引用類型,這就說明box1中的屬性和方法與box2中的屬性與方法雖然同名但卻不是一個引用,而是對Box對象定義的屬性和方法的一個複製。

咱們建立的每一個函數都有一個prototype屬性,這個屬性是一個指針,指向一個對象,這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法。那麼,prototype就是經過調用構造函數而建立的那個對象實例的原型對象。

使用原型的好處是可讓對象實例共享它所包含的屬性和方法。也就是說,沒必要在構造函數中添加定義對象信息,而是能夠直接將這些信息添加到原型中。使用構造函數的主要問題就是每一個方法都要在每一個實例中建立一遍。

JavaScript中,一共有兩種類型的值,原始值和對象值。每一個對象都有一個內部屬性 prototype ,咱們一般稱之爲原型。原型的值能夠是一個對象,也能夠是null。若是它的值是一個對象,則這個對象也必定有本身的原型。這樣就造成了一條線性的鏈,咱們稱之爲原型鏈。

函數能夠用來做爲構造函數來使用。另外只有函數纔有prototype屬性而且能夠訪問到,可是對象實例不具備該屬性,只有一個內部的不可訪問的__proto__屬性。__proto__是對象中一個指向相關原型的神祕連接。按照標準,__proto__是不對外公開的

當調用構造函數建立一個實例的時候,實例內部將包含一個內部指針(__proto__)指向構造函數的prototype,這個鏈接存在於實例和構造函數的prototype之間,而不是實例與構造函數之間。

function Person(name){                             //構造函數
    this.name=name;
}

Person.prototype.printName=function() {//原型對象
    alert(this.name);
}

var person1=new Person('Byron'); //實例化對象
console.log(person1.__proto__); //Person
console.log(person1.constructor); //Person
console.log(Person.prototype); //指向原型對象Person
var person2=new Person('Frank');

Person的實例person1中包含了name屬性,同時自動生成一個__proto__屬性,該屬性指向Personprototype,能夠訪問到prototype內定義的printName方法
圖片描述

實例就是經過構造函數建立的。實例一創造出來就具備constructor屬性(指向構造函數)和__proto__屬性(指向原型對象),

構造函數中有一個prototype屬性,這個屬性是一個指針,指向它的原型對象。
原型對象內部也有一個指針(constructor屬性)指向構造函數:Person.prototype.constructor = Person;

實例能夠訪問原型對象上定義的屬性和方法。
在這裏person1person2就是實例,prototype是他們的原型對象。

原型鏈的示意圖能夠用下圖來表示:
圖片描述

ES6


-

基本上,ES6class能夠看做只是一個語法糖,它的絕大部分功能,ES5均可以作到,新的class寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法而已。

class Point { //類名
    constructor(x, y) { //構造函數,constructor方法默認返回實例對象(即this)
        this.x = x; //this關鍵字表明實例對象
        this.y = y;
    }
    toString() { //prototype原型對象
        return '(' + this.x + ', ' + this.y + ')';
    }
}

typeof Point // "function"
Point === Point.prototype.constructor // true

上面代碼代表,類的數據類型就是函數,類自己就指向構造函數。

let point = new Point(1,2); //也是直接使用new命令,傳給constructor的值
point.toString(); //(1,2)
point.hasOwnProperty('x') //true,自身的屬性(由於定義在this變量上),
point.hasOwnProperty('toString') //false/此屬性是定義在原型上
point.__proto__.hasOwnProperty('toString') //true

point.constructor === Point.prototype.constructor //true

在類的實例上面調用方法,其實就是調用原型上的方法。

因爲類的方法都定義在prototype對象上面,因此類的新方法能夠添加在prototype對象上面。Object.assign方法能夠很方便地一次向類添加多個方法。

class Point {
    constructor(){ //能夠忽略不寫,會自動添加
        // ...
    }
}
Object.assign(Point.prototype, {
    toString(){},
    toValue(){}
});

prototype對象的constructor屬性,直接指向「類」的自己,這與ES5的行爲是一致的。

Point.prototype.constructor === Point //true,類內部的方法不能枚舉,和es5不同

繼承

Class之間能夠經過extends關鍵字實現繼承,這比ES5的經過修改原型鏈實現繼承,要清晰和方便不少。

class ColorPoint extends Point {}//至關於var ColorPoint=new Point也就是ColorPoint繼承了Point

上面代碼定義了一個ColorPoint類,該類經過extends關鍵字,繼承了Point類的全部屬性和方法。可是因爲沒有部署任何代碼,因此這兩個類徹底同樣,等於複製了一個Point類。es6的繼承是先新建父類的實例,再在子類中繼承修改this的指向

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); //調用父類的constructor(x, y)給父類傳值,用來新建父類的this對象。
    this.color = color;
  }
  toString() {
    return this.color + ' ' + super.toString(); // ES6 規定,經過super調用父類的方法時,super會綁定子類的this。
  }
}

let cp = new ColorPoint(25, 8, 'green');
cp instanceof ColorPoint // true
cp instanceof Point // true

ES5的繼承,實質是先創造子類的實例對象this,而後再將父類的方法添加到this上面Parent.apply(this)。
ES6的繼承機制徹底不一樣,實質是先創造父類的實例對象this(因此必須先調用super方法),而後再用子類的構造函數修改this。

繼承鏈
大多數瀏覽器的ES5實現之中,每個對象都有__proto__屬性,指向對應的構造函數的prototype屬性。ES6同時有prototype屬性和__proto__屬性,所以同時存在兩條繼承鏈。

(1)子類的__proto__屬性,表示構造函數的繼承,老是指向父類
(2)子類prototype屬性的__proto__屬性,表示方法的繼承,老是指向父類的prototype屬性。

class A {}

class B extends A {}

B.__proto__ === A  //true,es5中,是constructor,實例
B.prototype.__proto__ === A.prototype  //true,es5中,是B._proto_===A.prototype,方法

這樣的結果是由於,類的繼承是按照下面的模式實現的。

// B的實例繼承A的實例
Object.setPrototypeOf(B.prototype, A.prototype) = B.prototype.__proto__ = A.prototype;
const b = new B();

// B的實例繼承A的靜態屬性
Object.setPrototypeOf(B, A) = B.__proto__ = A;
const b = new B();

《對象的擴展》一章給出過Object.setPrototypeOf方法的實現。

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

這兩條繼承鏈,能夠這樣理解:做爲一個對象,子類(B)的原型(__proto__屬性)是父類(A);做爲一個構造函數,子類(B)的原型(prototype屬性)是父類的實例。

Object.create(A.prototype);
// 等同於
B.prototype.__proto__ = A.prototype;
    實例的__proto__屬性

子類實例的__proto__屬性的__proto__屬性,指向父類實例的__proto__屬性。也就是說,子類的原型的原型,是父類的原型。

var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');

p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true

三種特殊狀況。

第一種特殊狀況,子類繼承Object類。

class A extends Object {}

A.__proto__ === Object //true
A.prototype.__proto__ === Object.prototype //true

這種狀況下,A其實就是構造函數Object的複製,A的實例就是Object的實例。

第二種特殊狀況,不存在任何繼承。

class A {}
//由於A就是一個函數,因此它繼承的天然就是函數。就至關於new Function,但它的prototype是一個對象,因此繼承自對象

A.__proto__ === Function.prototype // true
A.prototype.__proto__ === Object.prototype // true

這種狀況下,A做爲一個基類(即不存在任何繼承),就是一個普通函數,因此直接繼承Funciton.prototype。可是,A調用後返回一個空對象(即Object實例),因此A.prototype.__proto__指向構造函數(Object)的prototype屬性。

第三種特殊狀況,子類繼承null

class A extends null {}

A.__proto__ === Function.prototype // true,代表new出來的都是函數,A是函數
A.prototype.__proto__ === undefined // true,由於繼承自null,因此它的_proto_找不着,就是undefined

這種狀況與第二種狀況很是像。A也是一個普通函數,因此直接繼承Funciton.prototype。可是,A調用後返回的對象不繼承任何方法,因此它的__proto__指向Function.prototype,即實質上執行了下面的代碼。

class C extends null {
  constructor() { return Object.create(null); 
}

原生構造函數的繼承

原生構造函數是指語言內置的構造函數,一般用來生成數據結構。ECMAScript的原生構造函數大體有下面這些。

Boolean()
Number()
String()
Array()
Date()
Function()
RegExp()
Error()
Object()

之前,這些原生構造函數是沒法繼承的。

class MyDate extends Date{

    getTest(){
        console.log('我是MyDate的擴展方法',this===date,new Date(),new MyDate(),)
        // this向的是它的實例對象,this===date
    }
}

let date=new MyDate();
console.log(date.getTime());//本地時間
date.getTest() //我是MyDate的擴展方法 true
相關文章
相關標籤/搜索