JavaScript:原型学习

一切皆对象

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

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

JavaScript:原型学习_第1张图片
Object-C元类图.jpg

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

JavaScript:原型学习_第2张图片
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);

关系图:


JavaScript:原型学习_第3张图片
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()
对象模型的细节
这三篇文章对于用原型实现继承说的比较详细

你可能感兴趣的:(JavaScript:原型学习)