类
这里说的类,在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有啥关系。