MS-JS:(二)从原点null扒清原型相关知识点

279 阅读8分钟
总会有这种感觉:别人问一个问题,我总是自己为是的理解,却无法描述出来。这就是一知半解的结果。MS(Memory and Sharing)系列是我自己学习笔记的分享和记录,不会对概念详细的探索和描述,旨在理解和记忆,并对好的资源进行收录以便查看!


ps: 不要小看概念的力量


1.引言

JS中有这样一句话:万物皆对象。就好像我们一个一个的人,人有起源,JS对象自然也有,所有对象的起点就是null。

为下文容易理解先做点铺垫:

  • JS中对象主要分为函数对象(Function)普通对象(Object)
  • 原型(prototype)简单而言:给其它对象提供共享属性的对象
  • __proto__是一个指针,指向的也是prototype。可以理解为prototype是显式原型,__proto__是隐式原型。
  • 所有的对象都有constructor__proto__,但是只有函数对象才有prototype


知道字多了你们没耐心,争取每个小知识点都配合图解。在加上文末的几个例子,相信认真看完的伙计门都会理解原型相关的知识点。


2.概念小记

2.1 函数对象和普通对象

在浏览器打印出fooobj两个对象的原型,如下:


结论:

  1. Function的实例都是函数对象,Object的实例都是普通对象。
  2. 函数对象(foo)也是对象,所以prototype__proto__都有,而普通对象(obj)只有只有__proto__ 。


2.2 prototype与__proto__ 

JavaScript 是一种基于原型的语言 (prototype-based language),这个和 Java 等基于类的语言不一样。

每个对象拥有一个原型对象,对象以其原型为模板,从原型继承方法和属性,这些属性和方法定义在对象的构造器函数的 prototype 属性上,而非对象实例本身。

来点代码:

// 先来个构造函数(函数对象),定义了name和age两个属性
function Person() {
    this.name = 'Jack'
    this.age = 18
}
// 原型上定义了name和sex两个属性
Person.prototype = {
    name: 'mary',
    sex: '女'
}

// 生成实例(普通对象),除了Function,所有的函数对象的实例都是普通对象
var student = new Person()

// 当获取构造函数和原型上都有的属性name时
console.log(student.name)   // 'Jack'
// 当获取构造函数上存在而原型上不存在的属性age时
console.log(student.age)   // '18'
// 当获取构造函数上不存在而原型上存在的属性sex时
console.log(student.age)   // '女'
结论:当对象自身上没有找到对应属性,便会在其原型上查找


再来瞅一瞅__proto__,再拿上面的例子来试试 :

// 先来个构造函数(函数对象),定义了name和age两个属性
function Person() {
    this.name = 'Jack'
    this.age = 18
}
// 原型上定义了name和sex两个属性
Person.prototype = {
    name: 'mary',
    sex: '女'
}

// 生成实例(普通对象),除了Function,所有的函数对象的实例都是普通对象
var student = new Person()

// 对象的__proto__属性是个指针,指向其构造函数的原型(prototype)
console.log(student.__proto__ === Person.prototype) // true


图解:


结论:对象(student)的__proto__是一个指向该对象构造函数(Person)原型(Person.prototype)的指针。


感兴趣的可以了解一下__proto__的来源:ECMAScript 规范说 prototype 应当是一个隐式引用:

1)通过 Object.getPrototypeOf(obj) 间接访问指定对象的 prototype 对象。

2)通过 Object.setPrototypeOf(obj, anotherObj) 间接设置指定对象的 prototype 对象。

3)部分浏览器提前开了 __proto__ 的口子,使得可以通过 obj.__proto__ 直接访问原型,通过 obj.__proto__ = anotherObj 直接设置原型。

4)ECMAScript 2015 规范只好向事实低头,将 __proto__ 属性纳入了规范的一部分。


2.3 prototype与constructor

引用《一文吃透所有JS原型相关知识点》中一段很清晰的描述

constructor属性也是对象所独有的,它是一个对象指向一个函数,这个函数就是该对象的构造函数。
注意,每一个对象都有其对应的构造函数,本身或者继承而来。单从constructor这个属性来讲,只有prototype对象才有。每个函数在创建的时候,JavaScript 会同时创建一个该函数对应的prototype对象,而函数创建的对象.__proto__ === 该函数.prototype,该函数.prototype.constructor===该函数本身,故通过函数创建的对象即使自己没有constructor属性,它也能通过__proto__找到对应的constructor,所以任何对象最终都可以找到其对应的构造函数。


3.图解原型(正题来了)

希望铺垫了半天,能让不熟悉的小伙伴对原型有个大致的概念,更好的理解下文:

3.1 混沌一片 - null

null是所有对象的起点,所有原型链的终点就是null。null也表示一个空的对象。



3.2 开天辟地 - Object

如果说null是起源,那Object就是祖先,如果开天辟地的盘古,Object是所有对象的祖先。


为了创造Object,先创造出其原型prototype,这里注意下图例,所有的原型对象(prototype)都是普通对象,其原型(隐式原型__proto__)指向null。


这里我们打印一下Object.prototype:


Object.prototype上便定义所有对象可以公用的属性和方法。为了下文方便,先给 Object.prototype 起个名字 - Original。


3.3 职能分离 - 造人的女娲Function

既然有函数对象和普通对象,所有又有了专职创造函数对象的Function。


此时,上帝Object的心态发生了点变化:我(Object)本身就是函数对象,难道我也是Function这小子造的?

既然造出来了就要承认,Object勉为其难的把自己的隐式原型(__proto__)指向了Function的原型(prototype)。



姬无命:我‘杀’了我自己?

Function: 我自己也是函数对象,我创造了我自己?于是把自己的隐式原型(__proto__)指向了自己的显式原型(prototype)。


这里我们可以得出两条结论:

  • Object.__proto__ === Function.__proto__
  • Function.__proto__ === Function.prototype


Function 和 Object 有种 ‘先有鸡还是先有蛋’ 的感觉,不用过分纠结。


3.4 众神就位,开始创世

有了’万物之始‘ Object 和 ’造物主‘ Function,便逐渐了又这缤纷的世间万物。


文初便说明过,只有Function的实例是函数对象 ,其他函数对象的实例都是普通对象。包括Object。


从上图我们可以得出许多常用的内置函数都是Function的实例,例如Array、Number,此外还有String、Number等等。所以其隐式原型(__proto__)都指向了Function.prototype。

验证一下:



然后便开始了全方位的造物:




这里需要新手注意一下,虽然上图重的2是一个基本数据类型,但是为了我们方便获取到其一些信息,javascript底层将其封装成一个对象。


3.5 血脉维系 - 原型链

前文中我们有过这样的结论:

当对象自身上没有找到对应属性,便会在其原型上查找

// 创建构造函数Person (函数对象)
function Person() {
    this.name = 'mother'
}
// 原型
Person.prototype = {
    age: 30
}

// 创建一个Person实例
var child = new Person()
console.log(child.name)


根据上文中的说明进行分析:child是一个普通对象,其构造函数Person是一个函数对象。其原型'传承'如下图红色箭头所示:



所以说:Object.prototype.__proto__ 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思。可以说原型链的顶端是null,也可以说是Object.prototype。


总结一哈原型链的定义:一个对象所拥有的属性不仅仅是它本身拥有的属性,它还会从其他对象中继承一些属性。当js在一个对象中找不到需要的属性时,它会到这个对象的父对象上去找,以此类推,这就构成了对象的原型链


4. 实题分析 - 加深理解

建议该部分自己动手打印一下结果看一下跟家容易理解。

4.1 Function.prototype === Function.__proto__

// 前文说过了,Function用来是实例出函数对象的,而他自己也是一个函数对象
// 自己生自己,本就是个有点小矛盾的事情,不需要过分纠结

console.log(Function.prototype === Function.__proto__)  // ture

4.2 Function.prototype === Object.__proto__

// Object本身也是个函数对象
// 虽然你的万物起源,但是有些事也要承认

console.log(Function.prototype === Object.__proto__)  // ture

4.3 constructor和prototype的关系

在上文图解过程中,没有加入constructor,是我觉得比较好理解。下面进行代码说明:

构造函数,简言之就是把你new出来的那个函数对象,注意这里是函数对象

/** 构造函数,就是把你new出来的那个函数对象 */
var Parent = new Function() // 这里用function关键字声明也是一样呀
/** Parent是被 Function 给 new 出来的,所以Parent的构造函数就是Function */

console.log(Parent.constructor === Function)


prototype和constructor的关系就是:prototypes上的constructor属性指向其构造函数。

/** 构造函数,就是把你new出来的那个函数对象 */
var Parent = new Function() // 这里用function关键字声明也是一样呀
/** Parent本身也是构造函数,也可以用来new出其他对象 */

console.log(Parent.prototype.constructor === Parent)
console.log(Function.prototype.constructor === Function)


普通对象也一样:

/** 构造函数,就是把你new出来的那个函数对象 */
var Parent = new Function() // 这里用function关键字声明也是一样呀
var child = new Parent()    // child是Parent实例
console.log(child.constructor === Parent)

// 由于普通函数没有prototype,所以下面一行代码会报错
console.log(child.prototype.constructor === child)


4.4 prototype.__proto__ 和 Object.prototype

// prototype是个普通对象,所以其__proto__指向Object.prototype
var Parent = new Function() // 这里用function关键字声明也是一样呀

console.log(Parent.prototype.__proto__ === Object.prototype)

Object.prototype.__proto__ === null


5. 资源参考


6. 结束语

关于prototype、__proto__以及constructor的关系,大家按着理解多这样加以尝试,相信对原型知识点还有迷惑的小伙伴都能有到收获。知道了原型相关知识点,下面将会总结一下继承相关的知识点。