JavaScript:原型学习

一切皆对象

面向对象的一个重要观点是:一切皆对象
如何做到这一点呢?如何建立继承体系呢?各种语言,C++、Java、Object-C、JavaScript、Swift一切皆结构)等等各自有不同的方法。
比如:

Object-C:通过3个实体(实例instance,类class,元类meta)和1个指针(isa)来实现。下面这张图是比较经典的:

Object-C元类图.jpg

JavaScript: 通过3个实体(对象,构造函数,原型)和3个指针(__proto__prototypeconstructor)来实现。同样,下面这张图也是比较经典的:

JavaScript原型链详图.gif

几个基础的概念

要理解上面那张经典图,需要理解下面这几个基本概念

  • 构造函数function Object() {...}是默认就存在的,不需要定义就能用。它的prototype指针指向原型Object.prototype
  • 原型Obeject.prototype作为对象看待,它的__proto__指针指向null。这个就是原型链的终点,也是经典图的最终出口。
  • 构造函数,比如系统的function Object() {...},或者自定义的function Foo() {...},也是对象,他们的构造函数是,(叫构造函数的构造函数也可以),function Function() {...},注意,这里的函数名是大写的Function,要和关键字function区分开来
  • “构造函数的构造函数”function Function() {...},也是函数,它的prototype指针指向原型Function.prototype
  • 所有的构造函数,包括“构造函数的构造函数”function Function() {...},都是对象,它们的__proto__指针,都指向了原型Function.prototype
  • 原型Function.prototype,也是对象,默认就是{};它的__proto__指针指向了Obeject.prototype
  • 自定义的对象,如果没有指定其原型是什么,默认也是{}。比如,在这里Function.prototype = {};,或者Function.prototype = new Object();。它的__proto__指针指向了Obeject.prototype
  • 构造过程new的实质:将原型赋值给对象的__proto__指针;
    例如var foo = new Foo(); 相当于做了如下这件事:foo.__proto__ = Foo.prototype;
  • 数组都继承于Array.prototype,(indexOf, forEach等方法都是从它继承而来)。
    var a = ["yo", "whadup", "?"];原型链如下:
    a ---> Array.prototype ---> Object.prototype ---> null
  • 函数都继承于Function.prototype,(call, bind等方法都是从它继承而来):
    function f() {...}原型链如下:
    f ---> Function.prototype ---> Object.prototype ---> null
  • 构造器其实就是一个普通的函数。当使用 new来作用这个函数时,它就可以被称为构造方法(构造函数)。

3个实体和3个指针

为了理解上面那张关系图,所以抽离出这几个概念。

3个实体

对象:这里其实指实例,只是JavaScript中习惯用对象这个词。看做是实例或者变量更容易理解一点。在命名上,推荐用小驼峰的方式,也就是变量的命名习惯。
__proto__指针
构造函数:JavaScript没有类,通过构造函数来构建对象。为了和其他语言保持一致,还引入了new。可以把构造函数看做是类,用this定义的都是成员变量。在命名上,推荐用大驼峰的方式,也就是类的命名习惯。
prototype指针。
__proto__指针(构造函数也是对象)。
原型:可以认为是JavaScript为了实现“继承”特性而引入的。简单理解,就是将所有实例共有的属性放在了原型上。作用相当于静态变量,静态函数以及基类的综合体。在命名上,推荐用“构造函数名.prototype”的形式。
constructor指针。
__proto__指针(原型也是对象)。

3个指针

__proto__: 对象的内置属性,这是一个指针,指向原型,也就是“构造函数名.prototype”
prototype: 构造函数的内置属性,这是一个指针,指向原型,也就是“构造函数名.prototype”
constructor: 原型的内置属性,这是一个指针,指向构造函数。

实际的例子

代码:

function Person(name, age){
    this.name = name;
    this.age = age;
}

Person.prototype.getInfo = function(){
    console.log(this.name + " is " + this.age + " years old");
}; 

will = new Person("Will", 28);  
wilber = new Person("WilBer", 27); 

// 可以看到这三个内容是一样的
console.dir(will.__proto__);
console.dir(wilber.__proto__);
console.dir(Person.prototype);

关系图:


Person关系图.png

属性查找

当查找一个对象的属性时,JavaScript会向上遍历原型链,直到找到给定名称的属性为止,到查找到达原型链的顶部(也就是 "Object.prototype"), 如果仍然没有找到指定的属性,就会返回 undefined

function Person(name, age){
    this.name = name;
    this.age = age;
}

Person.prototype.MaxNumber = 9999;
Person.__proto__.MinNumber = -9999;

var will = new Person("Will", 28);

console.log(will.MaxNumber); // 9999
console.log(will.MinNumber); // undefined

MaxNumber在原型上Person.prototype,能够找到;
Person.__proto__是指构造函数的原型,统一是Function.prototype,不在原型Person.prototype上,所以找不到。

属性隐藏

当通过原型链查找一个属性的时候,首先查找的是对象本身的属性,如果找不到才会继续按照原型链进行查找。
这样一来,如果想要覆盖原型链上的一些属性,我们就可以直接在对象中引入这些属性,达到属性隐藏的效果。

function Person(name, age){
    this.name = name;
    this.age = age;
}

Person.prototype.getInfo = function(){
    console.log(this.name + " is " + this.age + " years old");
};

var will = new Person("Will", 28);
will.getInfo = function(){
    console.log("getInfo method from will instead of prototype");
};

will.getInfo(); // getInfo method from will instead of prototype;

属性getInfo()在本地和原型上都有,原型上的属性被本地属性隐藏。这种效果跟子类覆盖父类的属性很相似。

属性遍历

"hasOwnProperty"是"Object.prototype"的一个方法,该方法能判断一个对象是否包含自定义属性而不是原型链上的属性,因为"hasOwnProperty" 是JavaScript 中唯一一个处理属性但是不查找原型链的函数。这个函数常常用在对象的属性遍历上面。

function Person(name, age){
    this.name = name;
    this.age = age;
}

Person.prototype.getInfo = function(){
    console.log(this.name + " is " + this.age + " years old");
};


var will = new Person("Will", 28);

for(var attr in will){
    console.log(attr);  // 本地和原型链上所有属性都输出
}
// name
// age
// getInfo

for(var attr in will){
    if(will.hasOwnProperty(attr)){
        console.log(attr); // 只输出本地属性,原型链上的属性不输出
    }
}
// name
// age

实现继承

主要是通过构造函数和Object.create()两种手段

方式1:使用构造函数

//Shape - superclass
function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.move = function(x, y) {
    this.x += x;
    this.y += y;
    console.info("Shape moved.");
};

// Rectangle - subclass
function Rectangle() {
  Shape.call(this); //call super constructor.
}

Rectangle.prototype = new Shape();

var rect = new Rectangle();

rect instanceof Rectangle //true.
rect instanceof Shape //true.

rect.move(1, 1); //Outputs, "Shape moved."

这种方式构造函数、对象、元素三种结构都能方便的表示,推荐使用。

方式2:使用Object.create()

ECMAScript 5 中引入了一个新方法:Object.create()
。可以调用这个方法来创建一个新对象。新对象的原型就是调用create方法时传入的第一个参数:

var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (继承而来)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因为d没有继承Object.prototype
  • 可以简单理解为Object.create()就是在原型链上面往上提了一级。
  • 由于cb没有构造函数,所以其原型没有构造函数.prototype的表示方法。不过可以用c.__proto__b.__proto__表示。注意,这两者是不一样的。可以认为两者都是匿名的。
  • a有默认的构造函数function Object() {...},所以他的原型可以表示为a.__proto__或者Object.prototype
  • 这个方式直接用对象当原型,减少了构造函数的参与,使用比较方便。
  • 用这种方式,原型只留下Object.prototype以及null这个出口
  • 在继承关系中,去掉了构造函数.prototype这个实体,直接用“实际的对象”替代,简化了“原型链”
  • 对于没有函数和共享变量(静态变量)的纯Model,推荐用这种简单的方式。将继承关系简单地画出一条原型链就可以理解。
  • 至于要加入函数和共享变量,那么就不能用构造函数.prototype这种方式来访问,应该改为对象.__proto__。在这种场景下,对于原型链的理解就很不方便了(一堆匿名的prototype)。目前来看,不推荐这种做法。
  • 如果对象.__proto__无法使用,可以通过对象.getPrototypeOf()代替,两者的效果是一样的。
  • 这种方式有限推荐,适用于{}定义的简单对象,Model这种既没有共享变量也没有方法的场景。

方案3:混合使用Object.create()和构造函数

//Shape - superclass
function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.move = function(x, y) {
    this.x += x;
    this.y += y;
    console.info("Shape moved.");
};

// Rectangle - subclass
function Rectangle() {
  Shape.call(this); //call super constructor.
}

Rectangle.prototype = Object.create(Shape.prototype);

var rect = new Rectangle();

rect instanceof Rectangle //true.
rect instanceof Shape //true.

rect.move(1, 1); //Outputs, "Shape moved."
  • 基本上和方案2一样,只有一个语句有差别:
    Rectangle.prototype = Object.create(Shape.prototype);
    Rectangle.prototype = new Shape();
  • Object.create(Shape.prototype);是用原型创建对象;
    new Shape();是用构造函数创建对象;
    这两者的效果是一样的。
  • Object.create(原型);相当于在原型链上往左走了一级;因为对象.__proto__构造函数.prototype这两个指针都指向了原型,在原型链上,对象和构造函数这两个实体要比原型这个实体更靠左一级

参考文章

彻底理解JavaScript原型
这篇文章写得比较好,值得好好看。将console.log() 改为 console.dir(),结构会看得更清晰一点。另外,那些比较===可以直接输入,不需要放在一个console.log()结构中。

javaScript原型链理解
里面的经典图就在这里用上了。

[objc 解释]:类和元类
这里的元类示意图还是不错的

继承与原型链
Object.create()
对象模型的细节
这三篇文章对于用原型实现继承说的比较详细

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

推荐阅读更多精彩内容