写在前面
之前看到过一个问题,大概意思是“如何创建一个纯净的对象”,看到这道题以后我有点懵,JavaScript 中不是万物皆对象?无论什么类型都可以是继承自 Object,也就是说拥有 Object 构造函数原型上的方法。
怎么还有纯净对象一说?后来了解了一下 Object 构造函数原型上的 API 才知道,原来有一个 Object.create(null)
方法可以初始化一个没有原型的对象。
之后还有看到一个问题,说“原型的终点是什么?”
好家伙,拉夫德鲁?最后了解了以后才终于知道,原来是 null。
但是我们可以看到一个奇怪的现象:
console.log(typeof null === 'object') // true
console.log(Object.prototype.__proto__ === null) // true
为什么是 null 的类型是对象,而对象原型链的终点却又是 null 呢?这其实是 JavaScript 这门语言的 bug。
在 JavaScript 最初版本使用的 32 位系统中会使用低位存储变量的类型信息,类型标签为 0 表示对象。
而 null 是一个空指针,以 000 开头,因此 null 的类型标签也被阴差阳错定义成了 0,于是便被识别为对象。
自此,null 走马上任,像极了封神榜里的张友仁。
简而言之,我名义上的儿子其实是我的爷爷。。。
因为 JavaScript 原型链种种有趣的事儿让我有了盘一盘的兴趣,让我们开始吧~
原型链
提到原型链就不得不去思考两个问题:
- 为什么要有原型链?
- 无论什么类型都有原型链吗?
原型链哪儿来的?
众所周知,最开始 JavaScript 只是为了验证浏览器表单而被设计出来的,因此在继承这一特性上,创始人并没有设计复杂的 class,只是借用了 C++ 和 Java 中 new 操作符的方式简单实现了继承。
但是问题来了,如果每个实例的成员变量都是私有的,那会造成数据太过独立无法共享,每生成一个实例就需要将构造函数中的成员都新创建一次,对于内存来说是极大的浪费。
于是,prototype 被设计了出来,将需要共享的成员放置于 prototype 中,私有变量放置于构造函数中。
但是,不能光有往下找的索引没有往上找的索引啊,constructor 就这样出现了。
也就是说,实例如果想要找到共享成员就需要去构造函数的 prototype 中查找。那么,怎么获取这个共享成员呢?_proto_ 由此被设计了出来。
原型链自此诞生~
误区
实例是否拥有 constructor 属性?
我们经常可以看到说实例通过 constructor 属性就可以找到自己的构造函数。
这句话没错,但是我们可以通过查看实例对象的具体属性发现,实例对象并没有一个叫做 constructor 的属性。
那么,我们常以为的 instance.constructor === Ctro
是怎么实现的?
其实,这一步是通过 _proto_ 桥接完成的。instance.constructor
本质上其实是 instance.__proto__.constructor
,因为实例的 _proto_ 指向了构造函数的原型,而原型对象是拥有 constructor 属性的。
考题分析 · 一
function Person() {}
Person.prototype.say = function() {}
const me = new Person()
const you = new Person()
console.log(me.constructor === Person) // true -> 都指向了 Person
console.log(you.constructor === Person) // true -> 都指向了 Person
console.log(me.constructor.prototype === me.__proto__) // true -> 都指向了 Person 的原型
console.log(me.__proto__.constructor === Person) // true -> 都指向了 Person
console.log(me.__proto__.constructor === you.constructor) // true -> 都指向了 Person
console.log(me.constructor.prototype === you.constructor.prototype) // true -> 都指向了 Person 的原型
console.log(me.__proto__ === you.__proto__) // true -> 都指向了 Person 的原型
console.log(me.__proto__.say === you.__proto__.say) // true -> 都指向了 Person 的原型上的 say 方法
无论什么类型都有原型链吗?
这个问题涉及到 JavaScript 的底层原理,JavaScript 最初被设计时就定义了所有的类型都是对象。
既然所有的类型都是对象那就说明它必定是 Object 的实例,间接说明,无论什么类型都有原型链。
不信?那你随便定义一个基本类型的变量,看看它是否拥有 _proto_ 属性~
题外话
_proto_ 是一个规范属性,但是该规范已经从 Web 标准中被删除,虽然如今依然可以在许多浏览器中被调用到,但为了安全起见,建议使用 Object.getPrototypeOf(instance)
或者 Reflect.getPrototypeOf(instance)
来代替~
考题分析 · 二
function Person(hobbies) {
this.hobbies = hobbies
}
Person.prototype.getHobbies = function() {
return this.hobbies
}
const somebodyA = new Person(['电影', '足球'])
const somebodyB = new Person(['CSGO'])
console.log(somebodyA.hobbies === somebodyB.hobbies) // false -> 一个是 ['电影', '足球'] 一个是 ['CSGO']
console.log(somebodyA.getHobbies() === somebodyB.getHobbies()) // false -> 一个是 ['电影', '足球'] 一个是 ['CSGO']
console.log(somebodyA.getHobbies === somebodyB.getHobbies) // true -> 都指向了 Person.prototype.getHobbies 方法
console.log(somebodyA.prototype === somebodyB.prototype) // true -> 都指向了 Person.prototype
考题分析 · 三 · (1)
由上面的讲解我们可以得知,实例获取共享成员的方式是通过 _proto_ 获取到构造函数原型上的成员,如果当构造函数身上不存在这个成员呢?
会一直往原型的原型(的原型的原型的原型的原型…)上找,直到找到找不到为止,返回 undefined
Object.prototype.name = 'aeorus'
function Person() {}
const me = new Person()
console.log(me.name) // aeorus
考题分析 · 三 · (2)
function Person() {}
Person.prototype.name = 'aiolos'
const me = new Person()
console.log(me.name) // aiolos
delete Person.prototype.name
console.log(me.name) // undefined
考题分析 · 三 · (3)
Object.prototype.name = 'aeorus'
function Person() {}
Person.prototype.name = 'aiolos'
const me = new Person()
console.log(me.name) // aiolos
delete Person.prototype.name
console.log(me.name) // aeorus
new 操作符
上面我提到说实例本身并不具有 constructor 属性,只有构造函数的原型才拥有。
对于实例本身来说,所能做的就是通过 _proto_ 来获取到构造函数的原型而已。
那么,new 操作符在实例化一个构造函数时到底做了哪些操作呢?
设置原型链
简单梳理一下,可以分为以下4种链条:
构造函数 --- prototype ---> 原型
构造函数 --- new ---> 实例
原型 --- constructor ---> 构造函数
实例 --- constructor ( 本质其实是 -> \__proto__.constructor ) ---> 构造函数
实例 --- \__proto__ ---> 原型
模拟 new 操作符的内部原理
- 构造函数中可以有返回值,如果是基本类型则忽略,如果是引用类型则将返回值覆盖构造函数
- 通过
Object.create(Ctro.prototype)
生成一个继承了 Ctro 原型的新对象 - 判断构造函数的调用的返回值类型
- 根据返回值类型返回实例
function Person(name) {
this.name = name
// return 1
// return [1, 2]
// return { a: 1 }
// return null
/* return () => {
console.log('a')
} */
}
Person.prototype.say = function() {
return this.name
}
function _createInstance() {
const Ctro = Array.from(arguments)[0]
if (!Ctro || typeof Ctro !== 'function') {
throw 'createInstance must receive a function for constructor'
}
// 获取构造函数的入参
const args = [].slice.call(arguments, 1)
// 获取拥有构造函数的原型的新对象
const newObject = Object.create(Ctro.prototype)
// 调用构造函数,将其 this 指向新对象,其目的有二
// 一是为了将构造函数内的成员绑定到新对象上
// 二是为了判断返回值是否为引用类型
const ctroReturnResult = Ctro.apply(newObject, args)
// 判断 ctroReturnResult 是否有返回值及其返回值类型
const isObject = typeof ctroReturnResult === 'object' && ctroReturnResult !== null
const isFunction = typeof ctroReturnResult === 'function'
// 如果有返回值,并且是非 null 的对象或者方法,则返回 ctroReturnResult
if (isObject || isFunction) return ctroReturnResult
// 如果没有,则返回 newObject
return newObject
}
const me = _createInstance(Person, 'aeorus')
继承
JavaScript 在设计之处没考虑过继承相关事宜,因此诞生了许多奇巧淫技的继承方法。
但我并不准备赘述太多的继承方法,在这只拿出最经典的三种继承方式以及 ES6 新增的 class 继承方式做介绍~
function Person(name, age) {
this.name = name
this.age = age
this.reference = ['black', 'white', 'gray']
}
Person.prototype.getInfo = function() {
return `My name is ${this.name}, and I'm ${this.age} years.`
}
原型链继承
- 既是父类的实例,也是子类的实例
- 父类成员共享程度太高,一个改变所有实例的该成员都会改变
- 子类在实例化父类作为自己的原型时无法传参
- 子类无法在实例化父类作为自己的原型前定义自己原型上的属性
- 子类只能继承一个父类
function Male(name, age) {
this.name = name
this.age = age
}
Male.prototype = new Person()
Male.prototype.getAge = function() {
return this.age
}
// test
const me = new Male('aeorus', 28)
const you = new Male('aiolos', 30)
console.log(me instanceof Person) // true
console.log(me instanceof Male) // true
me.getInfo() // My name is aeorus, and I'm 28 years. -> 虽然父类身上的 name 和 age 都是 undefined,但 this 会沿着作用域往上找
me.reference.pop()
console.log(me.reference) // ["black", "white"] -> 缺点
console.log(you.reference) // ["black", "white"] -> 缺点
组合继承
- 啥都好,就是
Person.call
了一次,然后又new Person
了一次,调用了两次
function Male(name, age) {
Person.call(this, name, age)
this.name = name
this.age = age
}
Male.prototype = new Person()
Male.prototype.constructor = Male
// test
const me = new Male('aeorus', 28)
const you = new Male('aiolos', 30)
console.log(me instanceof Person) // true
console.log(me instanceof Male) // true
me.getInfo() // My name is aeorus, and I'm 28 years. -> 虽然父类身上的 name 和 age 都是 undefined,但 this 会沿着作用域往上找
me.reference.pop()
console.log(me.reference) // ["black", "white"]
console.log(you.reference) // ["black", "white", "gray"]
寄生组合继承
function Male(name, age) {
Person.call(this, name, age)
this.name = name
this.age = age
}
const prototype = Object.create(Person.prototype)
prototype.constructor = Male
Male.prototype = prototype
// test
const me = new Male('aeorus', 28)
const you = new Male('aiolos', 30)
console.log(me instanceof Person) // true
console.log(me instanceof Male) // true
me.getInfo() // My name is aeorus, and I'm 28 years.
me.reference.pop()
console.log(me.reference) // ["black", "white"]
console.log(you.reference) // ["black", "white", "gray"]
ES6 class 继承
class Male extends Person {
constructor(name, age) {
super(name, age)
}
getAge() {
return this.age
}
getInfo() {
return super.getInfo() + ' And this message is in class Male'
}
}
// test
const me = new Male('aeorus', 28)
const you = new Male('aiolos', 30)
console.log(me instanceof Person) // true
console.log(me instanceof Male) // true
me.getInfo() // My name is aeorus, and I'm 28 years. And this message is in class Male
me.reference.pop()
console.log(me.reference) // ["black", "white"]
console.log(you.reference) // ["black", "white", "gray"]
手写 class
既然我们提到了 ES6 class 继承,那我们就想想如何实现这个内置的 class
class Parent {
constructor(name, age) {
this.name = name
this.age = age
}
getInfo() {
return `My name is ${this.name}, and I'm ${this.age} years.`
}
getName() {
return this.name
}
static getPrototype() {
return "Class constructor Parent cannot be invoked without 'new'"
}
}
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function")
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i]
descriptor.enumerable = descriptor.enumerable || false
descriptor.configurable = true
if ("value" in descriptor) descriptor.writable = true
Object.defineProperty(target, descriptor.key, descriptor)
}
}
function _createClass(Ctro, props, staticProps) {
if (props) _defineProperties(Ctro.prototype, props)
if (staticProps) _defineProperties(Ctro, staticProps)
return Ctro
}
var Parent = function () {
function Parent(name, age) {
_classCallCheck(this, Parent)
this.name = name
this.age = age
}
_createClass(Parent, [{
key: "getInfo",
value: function getInfo() {
return "My name is ".concat(this.name, ", and I'm ").concat(this.age, " years.")
}
}, {
key: "getName",
value: function getName() {
return this.name
}
}], [{
key: "getPrototype",
value: function getPrototype() {
return "Class constructor Parent cannot be invoked without 'new'"
}
}])
return Parent
}()
Object.create
在开篇我就提到一个问题——如何创建一个纯净的对象?我们现在知道是通过 Object.create(null)
方法达到的。
那么,这个方法的原理是什么呢?和我们这篇讲的原型又有什么关系呢?
就此,让我们对各种操作到原型的原生方法开始探索吧~
MDN 上面对于这个方法的定义为: 创建一个新对象,使用现有的对象来提供新创建的对象的 _proto_
有一说一,这句话有点拗口,但是我们可以看到 _proto_ 这个词,说明这里面藏着一个构造函数。
简单翻译一下就是: 传入一个对象,返回一个以这个对象为原型的实例。
这么一解释是不是就能很快手写一个了?
const _objectCreate = function(prototype) {
function F() {}
F.prototype = prototype
return new F()
}
const obj = _objectCreate({ name: 'aeorus' })
console.log(obj)
但是不要忘了,Object.create 拥有第二个可选入参 propertyObject,它是一个属性配置对象 ( 参考 Object.defineProperty 的第三个参数 ),该参数内定义的属性将被直接绑定到新对象身上。
因此,我们还需要去遍历第二个参数,通过 Object.defineProperty 的方式将其挂载到新对象身上。
const _objectCreate = function(prototype, propertyObject) {
function F() {}
F.prototype = prototype
const result = new F()
for (let key in propertyObject) Object.defineProperty(result, key, propertyObject[key])
return result
}
const obj = _objectCreate({ name: 'aeorus' }, {
// foo会成为所创建对象的数据属性
foo: {
writable:true,
configurable:true,
value: "hello"
},
// bar会成为所创建对象的访问器属性
bar: {
configurable: false,
get: function() { return 10 },
set: function(value) {
console.log("Setting `o.bar` to", value);
}
}
})
console.log(obj)