天天看點

javascript原型和繼承javascript原型和繼承

javascript原型和繼承

“一切都是對象”這句話的重點在于如何去了解“對象”這個概念。當然,也不是所有的都是對象,值類型就不是對象。

javascript

中判斷一個變量是不是對象?

function show(x) {
    console.log(typeof x);    // undefined
    console.log(typeof );   // number
    console.log(typeof 'abc'); // string
    console.log(typeof true);  // boolean
    console.log(typeof function () { });  //function
    console.log(typeof [, 'a', true]);  //object
    console.log(typeof { a: , b:  });  //object
    console.log(typeof null);  //object
    console.log(typeof new Number());  //object
}
show();
           

以上代碼列出了typeof輸出的集中類型辨別,其中上面的四種(

undefined

,

number

,

string

,

boolean

)屬于簡單的值類型,不是對象。剩下的幾種情況——函數、數組、對象、

null

new Number(10)

都是對象。 他們都是引用類型。

var fn = function () { };
console.log(fn instanceof Object);  // true
           

判斷一個變量是不是對象非常簡單。值類型的類型判斷用

typeof

,引用類型的類型判斷用

instanceof

有個疑問。在

typeof

的輸出類型中,

function

object

都是對象,為何卻要輸出兩種答案呢?都叫做

object

不行嗎?

函數和對象的關系

函數是一種對象,但是函數卻不像數組一樣——你可以說數組是對象的一種,因為數組就像是對象的一個子集一樣。但是函數與對象之間,卻不僅僅是一種包含和被包含的關系,函數和對象之間的關系比較複雜,甚至有一點雞生蛋蛋生雞的邏輯,

用案例來解答:

function Fn() {
    this.name = '小明';
    this.sex = 'male';
}
var fn1 = new Fn();
           

上面的這個例子很簡單,它能說明:對象可以通過函數來建立。

var obj = new Object();
obj.a = ;
obj.b = ;

var arr = new Array();
arr[] = ;
arr[] = 'x';
arr[] = true;

Object 和 Array 都是函數:
console.log(typeof (Object));  // function
console.log(typeof (Array));  // function
           

是以,可以很負責任的說——對象都是通過函數來建立的。

有個疑問:對象是函數建立的,而函數卻又是一種對象,函數和對象到底是什麼關系啊???

prototype原型

函數也是一種對象。他也是屬性的集合,你也可以對函數進行自定義屬性,

javascript

自己就先做了表率,人家就預設的給函數一個屬性——

prototype

。每個函數都有一個屬性叫做

prototype

這個

prototype

的屬性值是一個對象(屬性的集合,再次強調!),預設的隻有一個叫做

constructor

的屬性,指向這個函數本身。

javascript原型和繼承javascript原型和繼承

SuperType是是一個函數,右側的方框就是它的原型。

原型既然作為對象,屬性的集合,不可能就隻有

constructor

,肯定可以自定義的增加許多屬性。例如

Object

,他的

prototype

裡面,就有好幾個其他屬性。

javascript原型和繼承javascript原型和繼承

你也可以在自己自定義的方法的

prototype

中新增自己的屬性

function Fn() { }
Fn.prototype.name = '小明';
Fn.prototype.getSex = function () {
    return 'male';
};

var fn = new Fn();
console.log(fn.name);
console.log(fn.getSex());
           

Fn

是一個函數,

fn

對象是從

Fn

函數

new

出來的,這樣

fn

對象就可以調用

Fn.prototype

中的屬性。因為每個對象都有一個隐藏的屬性——“__proto__ ”,這個屬性引用了建立這個對象的函數的

prototype

。 即:

fn.__proto__ === Fn.prototype

這裡的

"__proto__ "

成為“隐式原型”

隐式原型

每個函數

function

都有一個

prototype

,即原型。這裡再加一句話——每個對象都有一個

__proto__

,可成為隐式原型。

這個

__proto__

是一個隐藏的屬性,

javascript

不希望開發者用到這個屬性值,有的低版本浏覽器甚至不支援這個屬性值。

javascript原型和繼承javascript原型和繼承

上面截圖看來,

obj.__proto__

Object.prototype

的屬性一樣!這麼巧!

答案就是一樣。

obj這個對象本質上是被

Object

函數建立的,是以

obj.__proto__ === Object.prototype

。我們可以用一個圖來表示。

javascript原型和繼承javascript原型和繼承

即,每個對象都有一個

__proto__

屬性,指向建立該對象的函數的

prototype

那麼上圖中的

“Object prototype”

也是一個對象,它的

__proto__

指向哪裡?

在說明

“Object prototype”

之前,先說一下自定義函數的

prototype

。自定義函數的

prototype

本質上就是和

var obj = {}

是一樣的,都是被

Object

建立,是以它的

__proto__

指向的就是

Object.prototype

但是

Object.prototype

确實一個特例——它的

__proto__

指向的是

null

javascript原型和繼承javascript原型和繼承

函數也是一種對象,函數也有

__proto__

嗎?

函數也是被建立出來的。誰建立了函數呢?

Function

注意這個大寫的

“F”

function fn(x,y) {
    return x + y;
};
console.log(fn(, ));

var fn1 = new Function('x' , 'y', 'return x + y;');
console.log(fn1(,));
           

以上代碼中,第一種方式是比較傳統的函數建立方式,第二種是用

new Functoin

建立。

根據上面說的一句話——對象的

__proto__

指向的是建立它的函數的

prototype

,就會出現:

Object.__proto__ === Function.prototype

。用一個圖來表示。圖中标出了:自定義函數

Foo.__proto__

指向

Function.prototype

Object.__proto__

指向

Function.prototype

Function.prototype

指向的對象,它的

__proto__

是不是也指向

Object.prototype

答案是肯定的。因為

Function.prototype

指向的對象也是一個普通的被

Object

建立的對象,是以也遵循基本的規則。

javascript原型和繼承javascript原型和繼承

介紹一下instanceof

先看例子

function Foo() {};
var f1 = new Foo();
console.log(f1 instanceof Foo); //true
console.log(f1 instanceof Object) //true 
           

Instanceof

運算符的第一個變量是一個對象,暫時稱為

A

;第二個變量一般是一個函數,暫時稱為

B

Instanceof

的判斷規則是:沿着

A

__proto__

這條線來找,同時沿着

B

prototype

這條線來找,如果兩條線能找到同一個引用,即同一個對象,那麼就傳回

true

。如果找到終點還未重合,則傳回

false

。是以

f1 instanceof Object

這句代碼就是

true

javascript原型和繼承javascript原型和繼承

介紹一下繼承

javascript

中的繼承是通過原型鍊來展現的

function Foo() { };
var f1= new Foo();
f1.a = ;
Foo.prototype.a = ;
Foo.prototype.b = ;
console.log(f1.a);
console.log(f1.b);
           

f1

Foo

函數

new

出來的對象,

f1.a

f1

對象的基本屬性,

f1.b

是怎麼來的呢?——從

Foo.prototype

得來, 因為

f1.__proto__

指向的是

Foo.prototype

通路一個對象的屬性時,先在基本屬性中查找,如果沒有,再沿着

__proto__

這條鍊向上找,這就是原型鍊。

javascript原型和繼承javascript原型和繼承

通路

f1.b

時,

f1

的基本屬性中沒有

b

,于是沿着

__proto__

找到了

Foo.prototype.b

。有疑問可以看看上一節将

instanceof

時候那個大圖

繼承的演變

ES3 => ES5 => ES6 的變化過程

因為我們現在用的

react

做的項目對

ES6

文法用的比較多,是以再詳細說一下:

extends

super

static

下面是

ES3

的寫法

function Student(props) {
  this.name = props.name || 'Unnamed';
}  //構造函數
Student.prototype.hello = function() {
  console.log('Hello, ' + this.name + '!');
} //原型



function PrimaryStudent(props) {
  Student.call(this, props);     //拿到Student構造函數的this.name = props.name || 'Unnamed';
  this.grade = props.grade || ;
} 

// es3實作繼承
function F() {}
F.prototype = Student.prototype;
PrimaryStudent.prototype = new F();
PrimaryStudent.prototype.constructor = PrimaryStudent;

PrimaryStudent.prototype.getGrade = function() {
  return this.grade;
}

var xiaoming = new PrimaryStudent({
  name: '小明',
  grade: '2',
});

console.log(xiaoming.name);  // 小明
console.log(xiaoming.grade) // 2

// 驗證原型:
console.log(xiaoming.__proto__  === PrimaryStudent.prototype) // true
console.log(xiaoming.__proto__ .__proto__  === Student.prototype) // true

// 驗證繼承關系
console.log(xiaoming instanceof PrimaryStudent); // true
console.log(xiaoming instanceof Student); // true
           

ES5

的繼承寫法:

function Student(props) {
  this.name = props.name || 'Unnamed';
}
Student.prototype.hello = function() {
  console.log('Hello, ' + this.name + '!');
}



function PrimaryStudent(props) {
  Student.call(this, props);
  this.grade = props.grade || ;
}

PrimaryStudent.prototype.getGrade = function() {
  return this.grade;
}

// es5實作繼承
PrimaryStudent.prototype = Object.create(Student.prototype);
PrimaryStudent.prototype.constructor = PrimaryStudent;

var xiaoming = new PrimaryStudent({
  name: '小明',
  grade: '2',
});

console.log(xiaoming.name);  // 小明
console.log(xiaoming.grade) // 2

// 驗證原型:
console.log(xiaoming.__proto__  === PrimaryStudent.prototype) // true
console.log(xiaoming.__proto__ .__proto__  === Student.prototype) // true

// 驗證繼承關系
console.log(xiaoming instanceof PrimaryStudent); // true
console.log(xiaoming instanceof Student); // true
           

ES6

的繼承寫法:

function Student(props) {
  this.name = props.name || 'Unnamed';
}
Student.prototype.hello = function() {
  console.log('Hello, ' + this.name + '!');
}


class PrimaryStudent extends Student {
  constructor(props) {
    super(props);
    this.grade = props.grade || ;
  }
  getGrade() {
    return this.grade;
  }
}

var xiaoming = new PrimaryStudent({
  name: '小明',
  grade: '2',
});

console.log(xiaoming.name);  // 小明
console.log(xiaoming.grade) // 2

// 驗證原型:
console.log(xiaoming.__proto__  === PrimaryStudent.prototype) // true
console.log(xiaoming.__proto__ .__proto__  === Student.prototype) // true

// 驗證繼承關系
console.log(xiaoming instanceof PrimaryStudent); // true
console.log(xiaoming instanceof Student); // true
           

extends 繼承内建類的能力

因為語言内部設計原因,我們沒有辦法自定義一個類來繼承

JavaScript

的内建類的。繼承類往往會有各種問題。

ES6

extends

的最大的賣點,就是不僅可以繼承自定義類,還可以繼承 JavaScript 的内建類,比如這樣:

class MyArray extends Array {
}
           

這種方式可以讓開發者繼承内建類的功能創造出符合自己想要的類。所有 Array 已有的屬性和方法都會對繼承類生效。這确實是個不錯的誘惑,也是繼承最大的吸引力。舉個例子:

var a = new Array(, , )
a.length  // 3
var b = new MyArray(, , )
b.length  // 3
           

super 在 constructor 和普通方法裡的不同

constructor

裡面,

super

的用法是

super(..)

。它相當于一個函數,調用它等于調用父類的

constructor

。但在普通方法裡面,

super

的用法是

super.prop

或者

super.method()

。它相當于一個指向對象的

__proto__

的屬性。這是

ES6

标準的規定,

如果寫子類的

constructor

需要操作

this

,那麼

super

必須先調用!這是

ES6

的規則。是以寫子類的

constructor

時盡量把

super

寫在第一行。

super

是編譯時确定(可以了解為定義時确定),不是運作時确定

總結:

如何記住原型鍊,這裡和

html

事件冒泡差不多

<null>
    <Object>//原型鍊頂端
        <parent>
            <child></child>
        </parent>
    </Object>
</null>
           

ES6

中繼承:

class Parent {
    constructor(firstName, lastName) { 
        console.log(firstName,lastName)
    }
    fullName() {
       return `${this.firstName} ${this.lastName}`
    }
}
class Child extends Parent {   //extends:不僅可以繼承自定義類,還可以繼承 JavaScript 的内建類;
  static findAll() {          //static: 該方法不會被執行個體繼承, 而是直接通過類來調用
 return super.findAll()
}
  constructor(firstName, lastName, age) {
    super(firstName, lastName);     // super: Parent.call(this, firstName, lastName)相當于函數執行Parent();
    this.age = age
  }
}
           

ES5

最經典的繼承方法是用組合繼承的方式,原型鍊繼承方法,借用函數繼承屬性,

ES6

也是基于這樣的方式,但是封裝了更優雅簡潔的

api

,讓

Javascript

越來越強大,修改了一些屬性指向,規範了繼承的操作,區分開了繼承實作和執行個體構造,此外

ES6

繼承還能實作更多的繼承需求和場景。

繼續閱讀