一切皆对象
面向对象的一个重要观点是:一切皆对象
如何做到这一点呢?如何建立继承体系呢?各种语言,C++、Java、Object-C、JavaScript、Swift
(一切皆结构)等等各自有不同的方法。
比如:
Object-C:通过3个实体(实例instance
,类class
,元类meta
)和1个指针(isa
)来实现。下面这张图是比较经典的:
JavaScript: 通过3个实体(对象,构造函数,原型)和3个指针(__proto__
、prototype
、constructor
)来实现。同样,下面这张图也是比较经典的:
几个基础的概念
要理解上面那张经典图,需要理解下面这几个基本概念
- 构造函数
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
会向上遍历原型链,直到找到给定名称的属性为止,到查找到达原型链的顶部(也就是 "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()
就是在原型链上面往上提了一级。 - 由于
c
和b
没有构造函数,所以其原型没有构造函数.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()
对象模型的细节
这三篇文章对于用原型实现继承说的比较详细