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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容