類
這里說的類,在ES6中討論的話,只有ES6的class關鍵字定義的一段封裝好的代碼才可以叫類,在ES6之前討論的話,是由構造函數和構造函數的原型語句組成的一套代碼。
構造函數
通過new操作符調用的函數就是構造函數。構造函數的特征一般是:
1、名稱首字母大寫。不是必須,只是為了開發者更快知道它是構造函數而俗成的約定。
2、即將被new。必須。
3、內部通常有this定義。不是必須。
4、外部通常有原型的定義。不是必須。
最簡單的構造函數是:
function Foo() {
}
prototype和__proto__
prototype叫函數原型,是函數(構造函數及其他函數)自帶的一個內部對象。它的主要作用是用于繼承別的對象(包括構造函數的原型對象)的屬性和方法,我們常說的“構造函數B繼承了構造函數A”,其實就是B的原型繼承了A的原型。比如:
var json = { // 隨便定義了一個對象json,有個方法a,值為11
a: 11
};
function Person(name,age) // 一個最簡的構造函數,簡單到沒有內容
{
}
Person.prototype = json; // 讓這個構造函數繼承json對象的屬性
console.log(new Person().a); // new一個實例對象,這個實例對象就有了屬性a,所以打印11
function Animal(name,age) // 一個空的父構造函數
{
}
Animal.prototype.a = 11; // 給它原型加了一個屬性
function Person(name,age) // 一個子構造函數
{
this.b = 22; // 給調用Person的對象定義一個屬性
}
Person.prototype = Animal.prototype; // 繼承Animal的原型
console.log(new Animal().a); // 11
console.log(new Animal().b); // undefined
console.log(new Person().a); // 11 // Person繼承了Animal
console.log(new Person().b); // 22
__proto__叫內部原型,是任何對象當然也包括函數自帶的一個對象,對比一下prototype的定義,prototype只是函數自帶的一個對象。那么我們看看:
function Person(name,age)
{
this.b = 22;
}
var a = {};
console.log(Person.prototype);
console.log(Person.__proto__);
console.log('------------------');
console.log(new Person().prototype);
console.log(new Person().__proto__);
console.log('------------------');
console.log(a.prototype);
console.log(a.__proto__);
結果是這樣的:
可見prototype和__proto__的區別是:
prototype只有函數(構造函數及其他函數)自帶,實例對象跟其他常規對象都不帶。
__proto__是函數(構造函數及其他函數)、實例對象、其他常規對象都自帶。
__proto__跟prototype的關系是:
Foo.prototype === new Foo().__proto__
function Person(name, age)
{
this.b = 22;
}
console.log(Person.prototype === new Person().__proto__);
注意到console.log(Person.__proto__);
的輸出了沒?是一個空函數。其實,所有構造函數/函數的__proto__都指向Function.prototype,它是一個空函數(Empty function)。也就是說,所有構造函數都繼承了Function.prototype的屬性及方法,如length、call、apply、bind(ES5新增)。再說白了,當你定義一個構造函數的那一刻,你就已經在用構造函數的繼承了。
上面說Function.prototype是空函數,空函數也是函數,它也有__proto__,會是什么呢?
console.log(Function.prototype.__proto__); // 空對象
console.log(Function.prototype.__proto__ === Object.prototype) // true
這說明所有的構造函數也都是普通對象,可以給構造函數添加/刪除屬性。同時它也繼承了Object.prototype上的所有方法:toString、valueOf、hasOwnProperty等。
那么Object.prototype的__proto__是誰?
console.log(Object.prototype.__proto__ === null); // true
已經到頂了,為null。因為null沒有原型也沒有內部原型。
這就是所謂“空生萬物”,即,空生對象,對象生函數。
上面研究的是函數的內部原型,下面研究一下實例對象的內部原型。
var obj = {name: 'jack'}
var arr = [1,2,3]
var reg = /hello/g
var date = new Date
var err = new Error('exception')
console.log(obj.__proto__ === Object.prototype) // true
console.log(arr.__proto__ === Array.prototype) // true
console.log(reg.__proto__ === RegExp.prototype) // true
console.log(date.__proto__ === Date.prototype) // true
console.log(err.__proto__ === Error.prototype) // true
結論是:函數實例的內部原型就是Function.prototype,數組實例的內部原型就是Array.prototype,其他都是這種道理。
constructor屬性
constructor屬性是任何對象都有的一個屬性。回憶一下,__proto__也是任何對象都有的一個對象。對象的constructor屬性返回創建該對象的構造函數的引用。證明如下:
var a = {};
var b = [];
var c = '';
var d = new Error();
console.log(a.constructor === Object); // true
console.log(b.constructor === Array); // true
console.log(c.constructor === String); // true
console.log(d.constructor === Error); // true
function e() {}
console.log(new e().constructor === e);
現在討論點好玩的。既然prototype是對象,所以prototype也自帶constructor屬性,下面我們就研究一下構造函數的prototype的constructor屬性。
一個構造函數只要存在,就肯定有prototype對象,它的prototype對象又肯定有constructor屬性,constructor屬性又指向構造函數,等于是個閉環:Foo.prototype.constructor === Foo
function Person(name,age)
{
this.b = 22;
}
console.log(Person.prototype.constructor === Person); // true
然后繼續推導,上面我總結過一個公式,Foo.prototype === new Foo().__proto__,所以得到:new Foo().__proto__.construator === Foo
function Person(name,age)
{
this.b = 22;
}
console.log(new Person().__proto__.constructor === Person); // true
再繼續推導,因為對象的constructor屬性返回創建該對象的構造函數的引用,所以,new Person('jack').constructor === Person
function Person(name) {
this.name = name
}
console.log(new Person('jack').constructor === Person);
由于Person.prototype === new Person('jack').__proto__,所以,new Person('jack').constructor.prototype === new Person('jack').__proto__,也就是說,對象的構造器屬性的原型等于內部原型。
function Person(name) {
this.name = name;
}
console.log(new Person('jack').constructor.prototype === new Person('jack').__proto__) // true
所以,new Person('jack').constructor.prototype === new Person('jack').__proto__ === Person.prototype就是最后的公式。
是不是很亂?所以有人用思維導圖的方式把這些串聯了起來。如果你到現在還沒有看懵,相信你就可以看懂那些思維導圖了。
如果原型方法被改寫,或者原型被整體重寫,會怎樣?
function Person(name) {
this.name = name;
}
var p1 = new Person('jack');
console.log(p1.__proto__ === Person.prototype); // true
console.log(p1.__proto__ === p1.constructor.prototype); // true
// 改寫原型方法
Person.prototype.getName = function() {return this.name + 1};
var p2 = new Person('jack');
console.log(p2.getName()); // jack1
console.log(p2.__proto__ === Person.prototype); // true
console.log(p2.__proto__ === p2.constructor.prototype); // true
// 重寫原型
Person.prototype = {
getName: function() {return this.name + 2}
};
var p3 = new Person('jack');
console.log(p3.getName()); // jack2
console.log(p3.__proto__ === Person.prototype); // true
console.log(p3.__proto__ === p3.constructor.prototype); // false
最后兩行輸出結果可以看出,p3.__proto__仍然指向的是Person.prototype,但不再指向p3.constructor.prototype。為什么?
給Person.prototype賦值的是一個對象直接量{getName: function(){}},使用對象直接量方式定義的對象其構造器(constructor)指向的是根構造器Object,Object.prototype是一個空對象{},{}自然與{getName: function(){}}不等。
給prototype添加的屬性和方法,跟給this添加的屬性和方法,有什么不同?
給prototype添加的屬性方法,不是構造函數自己的,而是外面來的。我說過,當一個構造函數聲明時,就算它是個空函數,你作為開發者其實已經使用了原型繼承,因為你的構造函數通過__proto__指向Function.prototype而繼承了Function.prototype的屬性和方法。這時候如果給構造函數的prototype另外添加屬性和方法,屬性和方法依然來自外部,也就是說,實例對象的屬性和方法來自于構造函數的prototype。
給this添加屬性和方法,實例對象創建的時候就已經獲得了這些屬性和方法,沒有中間步驟。也因此,構造函數里定義的this的屬性和方法,默認只能給自己的實例對象使用,如果想給構造函數的子構造函數使用,也得通過繼承。
私有屬性、私有方法
利用函數作用域的原理,構造函數內部定義的變量和函數,構造函數外部不可直接訪問,這就是私有屬性和私有方法。
function Foo() {
var a = 1;
function x() {
}
}
公有屬性、公有方法、特權屬性、特權方法
通過this創建的屬性和方法,所有實例都可以用,所以叫公有屬性、公有方法。由于公有屬性和方法能訪問私有屬性和方法,所以別名“特權屬性”、“特權方法”。
function Foo(name) {
this.a = name;
this.x = function () {
return this.name;
}
}
構造函數的屬性、方法,跟構造函數原型的屬性、方法,有什么不同?
構造函數的屬性、方法,跟實例沒關系,也不能被繼承,所以稱為靜態屬性、靜態方法。給構造函數自身加屬性和方法的意義,在于封裝。
function Foo() {}
Foo.a = 1;
var a = 1;
觀察上述代碼,Foo.a = 1;表明a屬性跟Foo是相關的,使用的時候就可以直接用Foo.a;
。如果不這樣,而是只寫var a = 1;,確實也能照樣使用這個變量a,但是從語義上講,我們根本看不懂a跟Foo有啥關系。