前言
写C++、Java、Objective-C等语言的猿人们,在接触JavaScript时一定被__proto__
和prototype
搞得晕头转向。笔者正在自学JavaScript,本文为JS对象学习小结,从传统“类继承”的角度来对比“原型继承”。修仙不易,且修且珍惜。
JS在设计之初并没有类
这个概念,ES6新增的class
关键字仍是基于原型继承
,而非真正的类继承
。既然没有class,JS对象是如何创建的?
对比
大部分面向对象语言是通过class extend other class
来完成继承的。即,用class创建对象,对象的属性及方法通过class的继承关系来查找。在这种模式下,对象的信息是以class为载体的。
JavaScript同样是一门面向对象语言,但它摒弃了class设计模式,采用更轻便的object link other object
模式完成继承。为什么说这种模式更轻便?先从对象
说起。
什么是对象?
你也许会说,class的实例就是对象。如果抛开class不谈,什么是对象?
对象是某一可以操纵属性及方法的具体事物。既然如此,对象和class并没有什么必然联系,所以class不是必需的,只是我们已经习惯了class的存在。如何创建对象?
var creature = {
name: "creature",
age: 0,
eat: function() {
console.log(this.name + " is eating")
}
}
creature.age // 0
creature.eat() // creature is eating
只要在大括号中以键值对的方式就可以描述属性及方法对应的信息,通过点语法可以操纵对象的属性及方法。由于对象和class无关,因此可以任意给对象增加或删除属性与方法(Objective-C中的对象虽然和class关联,但是由于runtime的存在,仍然可以在运行时动态改变class信息,这是由语言特性决定的。JavaScript与Objective-C属于动态语言,而C++与Java属于静态语言)
对象的属性及方法的删除与添加操作起来也很方便:
什么是继承?
你也许会说,让子类具有父类属性与方法的过程就是继承。OK... 抛开class这个概念不谈呢?
继承是让某一事物具备另一事物能力的过程。那么在没有class概念的情况下,JS如何实现继承特性?如何实现继承?
回到上文的object link other object
,JS中的继承是通过公共属性__proto__关联对象实现的:
var creature = {
name: "creature",
age: 0,
eat: function() {
console.log(this.name + " is eating")
}
}
var animal = {
name: "animal",
age: 10,
__proto__: creature
}
var plant = {
name: "plant",
age: 100,
__proto__: creature
}
animal.age // 10
animal.eat() // animal is eating
plant.age // 100
plant.eat() // plant is eating
对象中的公共__proto__属性和constructor方法
__proto__属性
每个对象都有__proto__
属性,将这个属性关联的对象称为原型
。因此,原型就是对象,也可称为原型对象
。从上面代码块可以看到,animal和plant的__proto__属性关联creature后,在未定义eat的情况下,仍然可以调用eat函数。那么JS对象是怎样寻找属性或者方法的?首先在当前对象中查找,如果没有,到当前对象的原型中查找,如果还没有,到当前对象原型的原型中查找,如此递归直到原型为null结束。这条链称为原型链
,如果没在原型链中找到对应的属性或方法,则返回undefined。(对比class的继承方式,两者寻找属性和方法的途径一致,只是class在“类”中递归查找,JS在原型中递归查找)-
constructor方法
每个对象都有constructor方法,constructor默认指向当前对象对应的构造函数。
可见,通过大括号创建的对象其实调用了Object构造函数。大括号创建对象的方式是一种语法糖,实际是通过Object构造函数创建的对象。(对比class中的构造函数,两者都是通过构造函数创建对象,只是class中的构造函数仍然是和“类”绑定在一起的,而JS中的构造函数是独立存在的)
构造函数
既然通过构造函数可以创建对象,那么构造函数有哪些特征?
- 为区别于普通函数,通常构造函数名首字母大写
- 构造函数必须通过
new
命令调用 - 构造函数内部使用
this
关键字,this指向当前构造函数生成的对象 - 构造函数没有
return
,默认返回this
以上文中的creature对象为例,如何通过自定义构造函数创建creature对象?
- 自定义构造函数
function Creature(name="creature", age=0){
this.name = name
this.age = age
this.eat = function() {
console.log(this.name + " is eating")
}
}
var creature = new Creature()
creature.age // 0
creature.eat() // creature is eating
基于万物皆对象
的思想理念,构造函数本身应该也是个对象(普通函数也是对象)。那么构造函数是由谁创建的?创建构造函数的那个事物又是由谁创建的?这样递归下去,似乎永远都找不到源头。构造函数的根究竟在哪里?
-
构造函数的constructor
以Creature构造函数为例,因为Creature也是个对象,所以可以通过内部的constructor访问到创建Creature构造函数的事物。即,Creature构造函数的构造函数:
可见,Creature构造函数的constructor是Function构造函数
,而Function构造函数的constructor是它本身。这样就解决了无限递归出现的可能,所以构造函数的源头就是Function构造函数。
既然构造函数是对象,那么构造函数的原型又是谁?
-
构造函数的__proto__
可见,普通函数与构造函数的原型都指向同一个匿名函数。这个匿名函数又是由谁创建的?
可见,不单构造函数是由Function构造函数创建的,在不手动指定constructor的情况下,任意函数的构造函数都是Function构造函数(包括Function构造函数本身)
函数与对象
- 区别与联系
1.函数属于对象
2.对象未必是函数
虽然函数也是对象的一种,但是作为JS中的一等公民,函数还存在一个特殊属性,prototype
。
- 函数中的prototype属性
prototype中存储的也是原型,但他不是当前函数对象的原型(当前函数对象的原型存储在__proto__属性中),如果仅把他当做函数看待,可以理解为函数的原型(注意区分函数与函数对象的概念),而函数的原型就指向函数生成的对象对应的原型:
function Creature(name="creature", age=0){
this.name = name
this.age = age
this.eat = function() {
console.log(this.name + " is eating")
}
}
var creature = new Creature()
可以看到,Creature构造函数的prototype属性确实指向构造函数生成的对象creature对应的原型。由于Creature.prototype就是creature的原型,所以Creature.prototype.constructor就是Creature构造函数本身。
上文已经说过,constructor的源头是Function构造函数,那么原型(对象)的源头又是谁?
- 原型(对象)的源头
同样,以creature对象为例:
function Creature(name="creature", age=0){
this.name = name
this.age = age
this.eat = function() {
console.log(this.name + " is eating")
}
}
var creature = new Creature()
可见,原型链的源头为null。也就是说,可以将对象的__proto__属性指向null,以此来创建一个对象。因为null中不存在任何属性和方法,所以创建出的对象没有继承到任何属性和方法。ES6中,提供了create方法来创建对象。
-
Object.create
可见,通过null确实能创建出对象,并且这个对象没有任何属性或方法,是最干净的对象。上文说过,通过大括号语法糖创建对象,其实会调用Object构造函数,这也是为什么我们创建出的绝大多数对象都拥有某些共同属性和方法。
- ES6中获取对象原型的方法
ES6提供了新的方法来获取和设置原型:getPrototypeOf
,setPrototypeOf
,这样我们不必直接操纵__proto__
属性:
以getPrototypeOf这个方法为例,他是通过Object构造函数调用的,所以必然存在于Object构造函数或者他的原型链中。因为是新增方法,并且仅能通过Object构造函数调用,猜测getPrototypeOf方法应该直接属于Object构造函数,而非继承自某个原型。那么,如何查看Object构造函数的属性及方法?
- 查看Object构造函数的属性与方法
通过上文我们已经知道Object.prototype.constructor === Object
,因此可以这样来查看:
可以看到,除getPrototypeOf与setPrototypeOf这两个方法外,之前提到的create方法,以及函数特有的属性prototype都在Object构造函数中。除此之外,Object构造函数中还提供了很多方便快捷的方法,这里不再赘述。
再次对比原型继承与类继承,其实两者很相似,越是通用的能力越沉到下层,越是特有的能力越浮在上层,只是JS摆脱了“类”这个枷锁。那么ES6中的class究竟是怎么回事?
ES6中的class
先来感受一下ES6中class的强大:
class Programmer {
constructor(name) {
this.name = name
}
sayHello() {
console.log("Hello, I'm " + this.name)
}
}
var p = new Programmer("Jack")
p.sayHello() // Hello, I'm Jack
class SeniorProgrammer extends Programmer {
constructor(name, skill) {
super(name)
this.skill = skill
}
level = "senior"
static keyboard() {}
}
ar sp = new SeniorProgrammer("Rose", "assembly")
sp.name // Rose
sp.skill // assembly
sp.sayHello() // Hello, I'm Rose
sp.level // senior
整个世界都更美好了有没有!虽然写法用法一样,然而这里的class真的是我们熟知的class吗?
可以看到,class SeniorProgrammer的原型为class Programmer,而class Programmer的原型为匿名函数,匿名函数的原型为Object构造函数。因此,这里的继承其实还是基于原型继承,而非类继承。
- class本质
可见,JS中的class并非真正的“类”,是由函数包装成的,而函数是对象的一种。
既然class是对象,那么他的构造函数和原型又是谁?
class的构造函数也是Function构造函数。如果class存在extends,那么class的原型为其extends对应的class。如果不存在extends,那么class的原型为Object构造函数的原型。
既然JS中的class仍然是以原型方式实现继承的,并且class本身是个函数,那么class中定义的属性及方法存储在哪里?
- 存储位置
写在constructor中由this.
生成的属性都存储在class创建的对象中,写在constructor之外的属性仍存储在由class创建的对象中,但是写在constructor之外的函数存储在class的原型中,static修饰的函数存储在类本身中。JS的class内部方法存储在原型中这点和“类继承”的面型对象很相似,这样不用每通过类创建一个对象就新开辟一块空间用来存储方法函数。
这里有一点值得关注,由于JS中的class其实是function,并非我们传统认知的class,所以当通过this.
某个属性赋值时,即使class的原型链有这个属性,仍然会在当前对象中生成新的属性来存储这个值,而非使用原型链上的值,这一点与“类继承”区别很大。
内置对象
作为补充,最后简单说一下JS中的内置对象(Math、JSON、Window)。
Math和JSON是平级的,两者的constructor都是Object构造函数,Object的constructor是Function构造函数,而Window的constructor就是Function构造函数。
Have fun!