面向对象编程的几个常见名词的解释

这里说的类,在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__);

结果是这样的:

Paste_Image.png

可见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就是最后的公式。

是不是很乱?所以有人用思维导图的方式把这些串联了起来。如果你到现在还没有看懵,相信你就可以看懂那些思维导图了。

Paste_Image.png

如果原型方法被改写,或者原型被整体重写,会怎样?

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有啥关系。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容