本文已参与「新人创作礼」活动,一起开启掘金创作之路
1. 遍历对象属性
使用for in循环可以遍历对象的key
var obj = {
name: 'plasticine',
age: 20,
}
for (var key in obj) {
console.log(key, obj[key])
}
2. Object.defineProperty
该方法可以用于定义一个对象的属性的规则,比如是否可以删除/修改,是否可以写入,是否可以枚举等等
var obj = {
name: 'plasticine',
age: 20,
}
Object.defineProperty(obj, 'temp', { value: 'tempValue' })
console.log(obj) // { name: 'plasticine', age: 20 }
console.log(obj.temp) // tempValue
第一个参数是要定义属性的对象
第二个参数是属性名,可以是字符串或Symbol
第三个是属性描述符,用于描述该属性的一些规则
属性描述符分为数据属性描述符和存取属性描述符
| configurable | enumerable | value | writable | get | set | |
|---|---|---|---|---|---|---|
| 数据属性描述符 | 可以 | 可以 | 可以 | 可以 | 不可以 | 不可以 |
| 存取属性描述符 | 可以 | 可以 | 不可以 | 不可以 | 可以 | 可以 |
2.1 数据属性描述符
[[Configurable]]:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符
- 当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true
- 当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false
[[Enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性
- 当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true
- 当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false
[[Writable]]:表示是否可以修改属性的值
- 当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true
- 当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false
[[value]]:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改
- 默认情况下这个值是undefined
var obj = {
name: 'plasticine',
age: 20,
}
Object.defineProperty(obj, 'address', {
value: '广州市', // 属性值
configurable: false, // 是否可以 删除/修改,以及是否可以将其修改为存取属性描述符
enumerable: false, // 是否可以通过 for in 循环或者 Object.keys() 遍历该属性
writable: false, // 是否可以修改属性值
})
console.log(obj) // { name: 'plasticine', age: 20, address: '广州市' }
// configurable 为 false 时无法删除属性
delete obj.address
console.log(obj) // { name: 'plasticine', age: 20, address: '广州市' }
// enumberable 为 false 时无法遍历到该属性
for (var key in obj) {
console.log(key) // name age
}
console.log(Object.keys(obj)) // [ 'name', 'age' ]
console.log(obj) // { name: 'plasticine', age: 20 }
// writable 为 false 时修改无效
obj.address = '深圳市'
console.log(obj) // { name: 'plasticine', age: 20, address: '广州市' }
2.2 存取属性描述符
相比于数据属性描述符,多了两个特性:get/set
注意:get/set与value/writable不能共存!!!
get/set就类似于Java的bean的getter/setter
何时会用存取属性描述符?
- 隐藏某一个私有属性,不希望直接被外界引用和赋值
- 想要截获某一个属性的访问和设置值的过程时
var obj = {
name: 'plasticine',
age: 20,
_address: '广州市',
}
Object.defineProperty(obj, 'address', {
configurable: true,
enumerable: true,
get: function () {
return this._address
},
set: function (value) {
this._address = value
},
})
/**
* {
name: 'plasticine',
age: 20,
_address: '广州市',
address: [Getter/Setter]
}
*/
console.log(obj)
console.log(obj.address) // 广州市
obj.address = '深圳市'
console.log(obj.address) // 深圳市
2.2.1 存取属性描述符的其他写法
可以在对象字面量上定义属性的Getter/Setter
// 字面量定义属性 Getter/Setter
var obj1 = {
name: 'obj1',
_age: 20,
get age() {
return this._age
},
set age(value) {
this._age = value
},
}
obj1.age = 666
console.log(obj1.age) // 666
console.log(obj1._age) // 666
3. Object.defineProperties
可以一次性对一个对象定义多个属性描述符
var obj = {
name: 'plasticine',
age: 20,
_address: '未知',
}
Object.defineProperties(obj, {
_address: {
enumerable: false,
},
address: {
configurable: true,
enumerable: true,
get: function () {
return this.name
},
set: function (address) {
this._address = address
},
},
gender: {
value: '男',
configurable: false,
enumerable: true,
writable: false,
},
})
4. Object的其他方法对对象进行限制
4.1 禁止对象添加新的属性
var obj = {
name: 'plasticine',
age: 20,
}
// 1. 禁止对象添加新的属性
Object.preventExtensions(obj)
obj.gender = '男'
console.log(obj) // { name: 'plasticine', age: 20 }
console.log(obj.gender) // undefined
4.2 禁止对象 配置/删除 属性,也能够禁止对象添加新的属性
控制configurable
// 2. 禁止对象 配置/删除 属性,也能够禁止对象添加新的属性
Object.seal(obj)
obj.gender = '男'
console.log(obj) // { name: 'plasticine', age: 20 }
console.log(obj.gender) // undefined
delete obj.name
console.log(obj) // { name: 'plasticine', age: 20 }
4.3 禁止对象修改属性
控制writable
// 3. 禁止对象修改属性
Object.freeze(obj)
obj.name = 'hahaha'
console.log(obj.name) // plasticine
4.4 查看对象的属性描述符
4.4.1 查找某一个属性的属性描述符
Object.getOwnPropertyDescriptor
console.log(Object.getOwnPropertyDescriptor(obj, 'name'))
// output
{
value: 'plasticine',
writable: false,
enumerable: true,
configurable: false
}
4.4.2 查找全部属性的属性描述符
console.log(Object.getOwnPropertyDescriptors(obj))
// output
{
name: {
value: 'plasticine',
writable: false,
enumerable: true,
configurable: false
},
age: { value: 20, writable: false, enumerable: true, configurable: false }
}
5. 批量创建对象
5.1 工厂模式创建
function createPerson(name, age) {
var person = new Object()
person.name = name
person.age = age
person.eating = function () {
console.log('eating...')
}
return person
}
var p1 = createPerson('张三', 20)
var p2 = createPerson('李四', 21)
var p3 = createPerson('王五', 22)
var p4 = createPerson('Plasticine', 23)
console.log(p1)
console.log(p2)
console.log(p3)
console.log(p4)
p1.eating()
p2.eating()
p3.eating()
p4.eating()
缺点:得到的对象是Object类型,没有自己的类型
5.2 构造函数创建
function Person(name, age) {
this.name = name
this.age = age
this.eating = function () {
console.log('eating...')
}
}
var p1 = new Person('张三', 20)
var p2 = new Person('李四', 21)
var p3 = new Person('王五', 22)
var p4 = new Person('Plasticine', 23)
console.log(p1) // Person { name: '张三', age: 20, eating: [Function (anonymous)] }
console.log(p2)
console.log(p3)
console.log(p4)
p1.eating()
p2.eating()
p3.eating()
p4.eating()
得到的对象是Person类型,解决了工厂模式的缺点
**缺点:**每个对象的方法应当是指向同一个函数对象,但实际上并非如此
console.log(p1.eating === p2.eating) // false
6. 原型
6.1 隐式原型
ES5以前,能够通过__proto__获取原型,注意:这种方式不是ECMA标准规定的,是浏览器自行实现的
function Person(name, age) {
this.name = name
this.age = age
}
var p1 = new Person('张三', 20)
var p2 = new Person('李四', 21)
// ES5 之前通过 __proto__ 获取原型(这个不是 ECMA 规范规定的,是浏览器自行实现的)
console.log(p1.__proto__) // {}
console.log(p2.__proto__) // {}
ES5以后,可以通过Object.getPrototypeOf()获得对象的隐式原型
// ES5 之后可以获得隐式原型
console.log(Object.getPrototypeOf(p1)) // {}
6.2 函数原型
函数有一个属性prototype,也叫显式原型
console.log(Person.prototype) // {}
默认是一个空对象
6.3 函数原型和对象的隐式原型的关系
6.3.1 new关键字做的事情
- 在内存中创建一个新对象
- 这个对象内部的
[[prototype]](就是隐式原型,双方括号[[]]表示是ECMA标准规定的属性)会被赋值为该函数对象的显式原型 - 构造函数内部的
this指向这个新创建的对象 - 执行构造函数内的代码
- 如果构造函数没有返回空对象,则会返回这个新创建的对象
通过以上过程,可以发现一个对象p1.__proto__是指向Person.prototype的
function Person(name, age) {
// new 内部添加的东西
this = {}
this.__proto__ = Person.prototype // __proto__ 就是 ECMA 规定的[[prototype]]
// 自己写的函数代码
this.name = name
this.age = age
// 除非自己写函数的时候显式返回了一个对象,否则 new 会默认返回它创造的新对象
return this
}
6.3.2 关系
根据这一点,就可以保证不同对象中的方法都指向同一个函数对象,只需要把方法放到构造函数的原型上声明即可
// 将方法放到函数原型上
Person.prototype.eating = function () {
console.log('eating...')
}
p1.eating()
p2.eating()
console.log(p1.eating === p2.eating) // true
6.4 函数原型的constructor
但是函数的原型对象本身并不是空的,而是有一个constructor属性的,由于它的enumberable被设置成false,因此直接打印Person.prototype是看不到的,可以用前面说过的Object.getOwnPropertyDescriptors查看
console.log(Object.getOwnPropertyDescriptors(Person.prototype))
console.log(Person.prototype.constructor)
// output
{
constructor: {
value: [Function: Person],
writable: true,
enumerable: false,
configurable: true
}
}
[Function: Person]
可以看到,constructor指向构造函数本身,并且enumerable被设置成false,所以直接打印会看不到
6.5 重写原型对象
当需要往构造函数原型上添加多个函数的时候,每次都要先写Person.prototype过于繁琐,因此可以考虑直接重写原型对象
function Person(name, age) {
this.name = name
this.age = age
}
// bad way to add function
Person.prototype.fn1 = function () {
//...
}
Person.prototype.fn2 = function () {
//...
}
Person.prototype.fn3 = function () {
//...
}
Person.prototype.fn4 = function () {
//...
}
// good way
Person.prototype = {
fn1: function () {
// ...
},
fn2: function () {
// ...
},
fn3: function () {
// ...
},
fn4: function () {
// ...
},
}
但是这样一来,就会把函数原型上的constructor覆盖掉,现在函数原型中就没有constructor了,不过没关系,既然我们知道constructor就是一个引用,指向构造函数本身,那我们手动加上不就行了吗?
// good way
Person.prototype = {
fn1: function () {
// ...
},
fn2: function () {
// ...
},
fn3: function () {
// ...
},
fn4: function () {
// ...
},
}
// 一定要在重写函数原型对象后添加 constructor,否则重写的时候又将其覆盖了
Object.defineProperties(Person.prototype, {
constructor: {
value: Person,
enumerable: false,
configurable: true,
writable: true,
},
})
console.log(Person.prototype)
console.log(Object.getOwnPropertyDescriptors(Person.prototype))
7. 原型链
7.1 原型链是什么
访问一个对象的属性的过程:
- 到这个对象自身中去找,如果没有,就到它的原型对象
[[prototype]]中去寻找 - 原型对象
[[prototype]]自身也是有原型对象的,即obj.__proto__.__proto__.(...一直找下去) - 但这个链不会一直链接下去,最终会到一个尽头的
这个尽头实际上就是**Object**的**[[prototype]]**
7.2 实现继承
7.2.1 用原型链实现继承
// 父类
function Person() {
this.name = 'plasticine'
}
Person.prototype.eating = function () {
console.log('Person is eating...')
}
// 子类
function Student() {
this.sno = 666
}
Student.prototype = new Person() // Student 继承自 Person
Student.prototype.studying = function () {
console.log('Student is studying...')
}
var stu = new Student()
stu.studying()
stu.eating()
实现继承的关键部分就在Student.prototype = new Person(),对应的内存图如下
这种方式实现继承有如下几个弊端:
- 打印stu对象的时候,看不到继承来的
**name**属性
// 弊端一:打印stu对象的时候,看不到继承来的 name 属性
console.log(stu) // Person { sno: 666 }
这是很不正常的,按照传统面向对象的思想,子类继承了父类,就应当能够看到父类中的属性,然而这里直接打印时,由于js只会打印对象的enumerable为true的属性,而不会去管它的原型上有什么属性。
且这里有另一个弊端,明明创建的对象是Student类型的,但是打印出来却是Person类型的
- 多个子类对象中的父类引用属性是共用的
假设现在父类中有一个数组属性,数组属性是一个引用类型的变量
// 父类
function Person() {
this.name = 'plasticine'
this.friends = []
}
那么子类既然继承自父类,自然也能够拥有friends这样一个属性,且不同对象中的friends属性也是不一样的,都是属于对象自己的才对
// 弊端二:多个子类对象中的父类引用属性是共用的
var stu1 = new Student()
var stu2 = new Student()
stu1.friends.push('plasticine')
console.log(stu1.friends) // [ 'plasticine' ]
console.log(stu2.friends) // [ 'plasticine' ]
但是这里就不对了,stu1修改了自己的friends,但却影响到了stu2中的friends
这是因为通过原型链的方式,stu1和stu2访问friends时,由于都是自身对象中不存在的属性
- 因此会到
[[prototype]]中寻找 - 而在
new Student()的时候,就将它们的[[prototype]]赋值成了Student.prototype了 - 由于
Student.prototype指向了new Person(),即指向这个匿名Person对象的[[prototype]] - 匿名Person对象的
[[prototype]]又指向了Person.prototype
所以整个原型链为stu1[[prototype]] -> Student.prototype -> 匿名Person对象的[[prototype]] -> Person.prototype。
friends这个属性放在匿名Person对象的[[prototype]]中,是一个引用类型变量,js引擎对于引用类型变量会直接使用,不会进行一个拷贝。
而如果是修改name这个属性的话,虽然name也是在匿名Person对象的[[prototype]]中,但是它是一个基础数据类型,js引擎会将它拷贝到stu对象中去,这时候name就是每个子类对象独有的了。
stu1.name = 'hahaha'
console.log(stu1.name) // name
console.log(stu2.name) // plasticine
- new子类构造函数的时候无法传递参数
// 弊端三:new子类构造函数的时候无法传递参数
var stu3 = new Student()
常规来说,既然Student继承自Person,那么实例化的时候,应当能够传入Person需要的参数。
比如我需要创建一个name='张三', age=20的Student对象,但是通过原型链实现继承的方式是无法做到这一点的
7.2.2 借用构造函数实现继承
为了解决**原型链实现继承**的三个弊端,可以采用如下方式实现继承:
// 父类
function Person(name, friends) {
this.name = name
this.friends = friends
}
// 子类
function Student(name, sno, friends) {
Person.call(this, name, friends)
this.sno = sno
}
Student.prototype = new Person() // Student 继承自 Person
子类中“借用”父类的构造方法去完成属性的赋值,一次性解决了上面的三个弊端!
// 解决弊端一:打印stu对象的时候,看不到继承来的 name 属性
console.log(stu) // Person { name: 'stu', friends: [ 'friend' ], sno: 66666 }
// 解决弊端二:多个子类对象中的父类引用属性是共用的
var stu1 = new Student('stu1', 1, ['friend1'])
var stu2 = new Student('stu2', 2, ['friend2'])
stu1.friends.push('plasticine')
console.log(stu1.friends) // [ 'friend1', 'plasticine' ]
console.log(stu2.friends) // [ 'friend2' ]
stu1.name = 'hahaha'
console.log(stu1.name) // hahaha
console.log(stu2.name) // stu2
// 解决弊端三:new子类构造函数的时候无法传递参数
var stu3 = new Student('plasticine', 666, ['mike'])
但是这种方式仍然有两个弊端:
- 父类构造函数至少被调用了两次
- 第一次是将父类对象赋值给子类构造函数的函数原型上时
- 第二次是new 子类构造函数时,会在子类构造函数内部再调用一次父类构造函数
- 子类对象的原型上会多出一些没有必要存在的属性
- 因为继承时,是将父类对象赋值给子类的构造函数的 --** **
Student.prototype = new Person(),这是一个无参构造,匿名Person对象中的属性全都是undefined - 这些属性完全用不到,没有必要存在
- 因为继承时,是将父类对象赋值给子类的构造函数的 --** **
那既然把子类构造函数的原型指向new 父类构造函数()返回的对象会有这两个弊端,那我们直接改成指向父类构造函数的原型不就好了吗?
Student.prototype = Person.prototype
这样子确实可行,但是有个更致命的问题,往子类构造函数原型上添加方法时,同时会添加到父类的构造函数原型中,因为子类构造函数原型是一个引用类型,指向父类的构造函数原型的,这就导致如果有别的子类继承这个父类构造函数原型,并且也是采用直接将其原型指向父类构造函数的原型的话,就会导致别的子类原型上的方法出现在自己的原型上,这显然是不符合面向对象继承的规则的。
7.2.3 原型式继承 -- 针对对象
根据前面两种方式的缺点,目前我们要解决的问题就是子类对象继承自父类,并且往子类原型中添加的方法不会添加到父类原型上,前面有通过new一个父类对象构造函数的无参构造的方式作为子类原型,虽然能够保证子类原型中添加的方法不会添加到父类原型上,但是会导致执行不必要的父类构造函数。
那么如果能够new出一个空对象,这个空对象的[[prototype]]是指向父类构造函数原型的话,不就解决这个问题了吗?
这就要用到原型式继承了
var father = {
name: '父类对象',
}
/**
* 创建一个对象,且这个对象的原型指向传入的对象 p 的原型
* @param {object} p 父类对象
*/
function createObject(p) {
function fn() {}
fn.prototype = p
return new fn()
}
var son1 = createObject(father)
console.log(Object.getPrototypeOf(son1)) // { name: '父类对象' }
/**
* 创建一个对象,且这个对象的原型指向传入的对象 p 的原型 -- 使用 Object.setPrototypeOf
* @param {object} p 父类对象
*/
function createObject2(p) {
var obj = {}
Object.setPrototypeOf(obj, p)
return obj
}
var son2 = createObject2(father)
console.log(Object.getPrototypeOf(son2)) // { name: '父类对象' }
// 使用 Object.create -- 和上面的方式等价
var son3 = Object.create(father)
console.log(Object.getPrototypeOf(son3)) // { name: '父类对象' }
注意:原型式继承是要有父类对象的前提下才行,如果没有父类对象则不能用这种方式创建子类对象
7.2.4 寄生式继承 -- 针对对象
采用原型式继承时,如果子类对象需要赋值自己的属性,就需要像下面这样:
var father = {
name: '父类对象',
}
// 使用 Object.create -- 和上面的方式等价
var son = Object.create(father)
son.name = 'plasticine'
son.age = 20
console.log(son)
属性少还好说,如果属性多则需要一大堆的显式赋值的代码,而且如果是要创建多个对象就更加恐怖了,因此出现了寄生式继承这种方案,其实就是将子类对象的创建写在一个工厂函数里面
var father = {
name: '父类对象',
}
function createSon(p, name, age) {
var son = Object.create(p)
son.name = name
son.age = age
return son
}
var son = createSon(father, 'plasticine', 20)
console.log(son) // { name: 'plasticine', age: 20 }
但这样子的话,又会出现前面批量创建对象 -- 工厂模式创建中的弊端了
7.2.5 寄生组合式继承 -- 最终方案
// 父类构造函数和方法
function Parent(name, age) {
this.name = name
this.age = age
}
Parent.prototype.eating = function () {
console.log('父类方法 -- eating...')
}
// 子类构造函数和方法
function Son(name, age, habbit) {
Parent.call(this, name, age)
this.habbit = habbit
}
// 让子类构造函数通过原型继承父类的核心代码
Son.prototype = Object.create(Parent.prototype)
// 修改子类构造函数原型的 constructor 为自己的构造函数,让实例化的对象拥有正确的类型
Object.defineProperty(Son.prototype, 'constructor', {
value: Son,
writable: true,
configurable: true,
enumerable: false,
})
Son.prototype.playing = function () {
console.log('子类方法 -- playing...')
}
var son = new Son('plasticine', 20, 'coding')
console.log(son) // Son { name: 'plasticine', age: 20, habbit: 'coding' }
son.eating() // 父类方法 -- eating...
son.playing() // 子类方法 -- playing...
可以发现,其实要实现继承,主要就是要有这两行代码:
// 让子类构造函数通过原型继承父类的核心代码
Son.prototype = Object.create(Parent.prototype)
// 修改子类构造函数原型的 constructor 为自己的构造函数,让实例化的对象拥有正确的类型
Object.defineProperty(Son.prototype, 'constructor', {
value: Son,
writable: true,
configurable: true,
enumerable: false,
})
那么我们就可以将这部分逻辑抽离成一个函数,暂且叫它inheritPrototype吧
/**
* 实现子类继承父类
* @param {Function} SubConstructor 子类构造函数
* @param {Function} ParentConstructor 父类构造函数
*/
function inheritPrototype(SubConstructor, ParentConstructor) {
// 让子类构造函数通过原型继承父类的核心代码
SubConstructor.prototype = Object.create(ParentConstructor.prototype)
// 修改子类构造函数原型的 constructor 为自己的构造函数,让实例化的对象拥有正确的类型
Object.defineProperty(SubConstructor.prototype, 'constructor', {
value: SubConstructor,
writable: true,
configurable: true,
enumerable: false,
})
}
在子类和父类构造函数都写好的时候,只需要调用这个函数在他们的原型之间建立继承关系即可
inheritPrototype(Son, Parent)
8. 对象方法补充
8.1 hasOwnProperty
用于判断一个属性是否属于对象本身,而不是对象的原型链上的
var foo = {
name: 'plasticine',
age: 20,
}
var bar = Object.create(foo)
bar.myAttr = 'myAttr'
console.log(bar.hasOwnProperty('myAttr')) // true
console.log(bar.hasOwnProperty('name')) // false
8.2 in/for in操作符
in用于判断属性是否在对象中,无论是对象自身的还是说在原型链上
var foo = {
name: 'plasticine',
age: 20,
}
var bar = Object.create(foo)
bar.myAttr = 'myAttr'
// in
console.log('myAttr' in bar) // true
console.log('name' in bar) // true
for in用于遍历一个对象的属性
// for in
for (var key in bar) console.log(key) // myAttr name age
8.3 instanceOf操作符
用于检测构造函数的prototype,是否出现在某个实例对象的原型链上
inheritPrototype(Son, Parent)
var son = new Son('plasticine', 20, 'coding')
console.log(son instanceof Son) // true
console.log(son instanceof Parent) // true
8.4 isPrototypeOf
用于检测某个对象,是否出现在某个实例对象的原型链上
inheritPrototype(Son, Parent)
var son = new Son('plasticine', 20, 'coding')
console.log(son.isPrototypeOf(Son.prototype)) // false
console.log(Son.prototype.isPrototypeOf(son)) // true
console.log(Parent.prototype.isPrototypeOf(son)) // true
9. 对象-函数-原型的关系
首先要明确一下以下几个概念:
- 对象有隐式原型
[[prototype]],在浏览器或node实现中,可以用__proto__来查看
var foo = {
name: 'foo',
}
console.log(foo.__proto__) // [Object: null prototype] {}
- 字面量创建对象等价于
new Object()调用Object构造函数来创建对象
var foo = {
name: 'foo',
}
var bar = new Object()
bar.name = 'bar'
console.log(foo.__proto__) // [Object: null prototype] {}
console.log(bar.__proto__) // [Object: null prototype] {}
- 对象的
__proto__指向构造函数的prototype
function Foo(name, age) {
this.name = name
this.age = age
}
var foo = new Foo('plasticine', 20)
console.log(foo.__proto__ === Foo.prototype) // true
- 函数也是对象,是对象就会有
__proto__隐式原型
// 函数也是对象,是对象就会有__proto__隐式原型
function Foo(name, age) {
this.name = name
this.age = age
}
console.log(Foo.__proto__) // {}
console.log(Foo.prototype) // {}
console.log(Foo.__proto__ === Foo.prototype) // false
- 函数等价于
new Function()创建的对象
// 这两种形式的函数等价,因此函数也是对象,且是 Function 对象
var Foo = new Function()
function Foo() {}
console.log(Foo.__proto__)
console.log(Foo.prototype)
console.log(Foo.__proto__ === Foo.prototype)
- Function函数对象的隐式原型和函数原型相等
console.log(Function.__proto__ === Function.prototype) // true
- Object原型对象的隐式原型为
null
console.log(Object.prototype.__proto__) // null
明确了上面七点后,就能理解下面这幅图了,而这幅图能够很好地说明对象-函数-原型之间的关系,图片来自mollypages.org/