一文看懂JavaScript面向对象(深入原型、原型链和继承)

646 阅读17分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

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 第三个是属性描述符,用于描述该属性的一些规则 属性描述符分为数据属性描述符存取属性描述符

configurableenumerablevaluewritablegetset
数据属性描述符可以可以可以可以不可以不可以
存取属性描述符可以可以不可以不可以可以可以

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/setvalue/writable不能共存!!! get/set就类似于Java的bean的getter/setter

何时会用存取属性描述符

  1. 隐藏某一个私有属性,不希望直接被外界引用和赋值
  2. 想要截获某一个属性的访问和设置值的过程时
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关键字做的事情

  1. 在内存中创建一个新对象
  2. 这个对象内部的[[prototype]](就是隐式原型,双方括号[[]]表示是ECMA标准规定的属性)会被赋值为该函数对象的显式原型
  3. 构造函数内部的this指向这个新创建的对象
  4. 执行构造函数内的代码
  5. 如果构造函数没有返回空对象,则会返回这个新创建的对象

通过以上过程,可以发现一个对象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 原型链是什么

访问一个对象的属性的过程:

  1. 到这个对象自身中去找,如果没有,就到它的原型对象[[prototype]]中去寻找
  2. 原型对象[[prototype]]自身也是有原型对象的,即obj.__proto__.__proto__.(...一直找下去)
  3. 但这个链不会一直链接下去,最终会到一个尽头的

这个尽头实际上就是**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(),对应的内存图如下 image.png 这种方式实现继承有如下几个弊端:

  1. 打印stu对象的时候,看不到继承来的**name**属性
// 弊端一:打印stu对象的时候,看不到继承来的 name 属性
console.log(stu) // Person { sno: 666 }

这是很不正常的,按照传统面向对象的思想,子类继承了父类,就应当能够看到父类中的属性,然而这里直接打印时,由于js只会打印对象的enumerable为true的属性,而不会去管它的原型上有什么属性。 且这里有另一个弊端,明明创建的对象是Student类型的,但是打印出来却是Person类型的

  1. 多个子类对象中的父类引用属性是共用的

假设现在父类中有一个数组属性,数组属性是一个引用类型的变量

// 父类
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时,由于都是自身对象中不存在的属性

  1. 因此会到[[prototype]]中寻找
  2. 而在new Student()的时候,就将它们的[[prototype]]赋值成了Student.prototype
  3. 由于Student.prototype指向了new Person(),即指向这个匿名Person对象的[[prototype]]
  4. 匿名Person对象的[[prototype]]又指向了Person.prototype

所以整个原型链为stu1[[prototype]] -> Student.prototype -> 匿名Person对象的[[prototype]] -> Person.prototypefriends这个属性放在匿名Person对象的[[prototype]]中,是一个引用类型变量,js引擎对于引用类型变量会直接使用,不会进行一个拷贝。 而如果是修改name这个属性的话,虽然name也是在匿名Person对象的[[prototype]]中,但是它是一个基础数据类型,js引擎会将它拷贝到stu对象中去,这时候name就是每个子类对象独有的了。

stu1.name = 'hahaha'
console.log(stu1.name) // name
console.log(stu2.name) // plasticine
  1. 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'])

但是这种方式仍然有两个弊端:

  1. 父类构造函数至少被调用了两次
    1. 第一次是将父类对象赋值给子类构造函数的函数原型上时
    2. 第二次是new 子类构造函数时,会在子类构造函数内部再调用一次父类构造函数
  2. 子类对象的原型上会多出一些没有必要存在的属性
    1. 因为继承时,是将父类对象赋值给子类的构造函数的 --** **Student.prototype = new Person(),这是一个无参构造,匿名Person对象中的属性全都是undefined
    2. 这些属性完全用不到,没有必要存在

那既然把子类构造函数的原型指向new 父类构造函数()返回的对象会有这两个弊端,那我们直接改成指向父类构造函数的原型不就好了吗?

Student.prototype = Person.prototype

这样子确实可行,但是有个更致命的问题,往子类构造函数原型上添加方法时,同时会添加到父类的构造函数原型中,因为子类构造函数原型是一个引用类型,指向父类的构造函数原型的,这就导致如果有别的子类继承这个父类构造函数原型,并且也是采用直接将其原型指向父类构造函数的原型的话,就会导致别的子类原型上的方法出现在自己的原型上,这显然是不符合面向对象继承的规则的。


7.2.3 原型式继承 -- 针对对象

根据前面两种方式的缺点,目前我们要解决的问题就是子类对象继承自父类,并且往子类原型中添加的方法不会添加到父类原型上,前面有通过new一个父类对象构造函数的无参构造的方式作为子类原型,虽然能够保证子类原型中添加的方法不会添加到父类原型上,但是会导致执行不必要的父类构造函数。 那么如果能够new出一个空对象,这个空对象的[[prototype]]是指向父类构造函数原型的话,不就解决这个问题了吗? PNG图像.png 这就要用到原型式继承

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. 对象-函数-原型的关系

首先要明确一下以下几个概念:

  1. 对象有隐式原型[[prototype]],在浏览器或node实现中,可以用__proto__来查看
var foo = {
  name: 'foo',
}

console.log(foo.__proto__) // [Object: null prototype] {}
  1. 字面量创建对象等价于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] {}
  1. 对象的__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
  1. 函数也是对象,是对象就会有__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
  1. 函数等价于new Function()创建的对象
// 这两种形式的函数等价,因此函数也是对象,且是 Function 对象
var Foo = new Function()
function Foo() {}

console.log(Foo.__proto__)
console.log(Foo.prototype)
console.log(Foo.__proto__ === Foo.prototype)
  1. Function函数对象的隐式原型和函数原型相等
console.log(Function.__proto__ === Function.prototype) // true
  1. Object原型对象的隐式原型为null
console.log(Object.prototype.__proto__) // null

明确了上面七点后,就能理解下面这幅图了,而这幅图能够很好地说明对象-函数-原型之间的关系,图片来自mollypages.org/ jsobj_full.jpg jsobj2-full.png