原型链与原型属性
JS 是一种有趣的编程语言
话说,哪一门编程语言都挺有趣的,没点看家本领与有趣之处的语言,根本不会被我知晓。
JS 最有趣的地方在于它的原型链机制。
与 Python 一样,JS 中也是一切皆为对象;但与 Python 不一样地方是,JS 中并没类的概念,而一切的重担就都落到了 「函数」 身上。
JS 一切皆对象,每一个对象都是一个独立的上下文,上下文中包含三个部分:
- 值属性
- 方法属性
- 作用域变量
其中的属性除了在对象所处上下文中直接获取之外,还会沿着原型链一直往上追溯,末端是 Object.prototype 。如果没有,就是 undefined。(此处值得注意的是,这里的 undefined 不是 null,null 是一个空值,但的确代表一个值,而 undefined 代表未定义,但很诡异的是,向一个变量赋值 undefined 是允许的。)
每一个对象当中,除了属性之外,还会作用域变量,可以是局域的,也可以是全局的,还可以是闭包的,甚至在最新 ES6 中还有了 块域变量。
值得思考的是,这些作用域变量和属性之间是什么关系?
属性明显是针对「对象」,而作用域针对函数。
而在 JS 中,函数的独特性就体现在了这里,函数可以有多重角色:
- 函数
- 实例方法
- 实例构造器
- 临时作用于某个实例对象,也就是某个上下文
函数在创建时,都会附带一个上下文,这个上下文中可以有属性,也可以有作用域变量。因为函数既是函数,也是对象。
一个已经被创建的函数,已经自带一套上下文,但函数在运行时可以被临时指派到其它上下文中。一般而言,我们用 this 指代上下文,用 apply、call 和 bind 三种彼此有些差别的方法属性指派临时上下文。
与此同时,一个函数还可以被用来构造一个对象实例,构造完成之后,实例便拥有了函数对象中定义好的属性和方法,但是作用域变量会丢失,也就是说,在构造实例中,作用域变量不起作用。
这样我们拥有了两样东西:
- 构造函数
- 实例
- 实例拥有构造函数中的值属性与方法属性
此时,就到了 JS 中的大 boss 「原型链」出场了。原型也是一个对象,而且非常关键的一点是,所有原型都依托于构造函数,也就是说「原型与构造函数成对出现」。
这样,我们就又有了下面的关系:
- 构造函数有一个属性:prototype,属性的值对应一个「原型对象」
- 原型对象也有一个属性:constructor,属性的值对应一个「构造函数」
- 构造函数生成的所有实例,其原型都指向构造函数 prototype 属性
- 因为实例的属性都来自于构造函数的属性,所以构造函数的 prototype 就是实例的第一个原型
- 但我们通常不使用实例的原型属性获取原型,原因很简单,并不是所有的实例都是由构造函数生成,所以在追溯原型链的过程中,会止步于第一个原型
- 我们使用一种 __proto__ 的方法属性追溯原型链
既然是原型链,那就有一个顶端,以及一个原生的原型链。位于顶端的原型是 Object.prototype, 它的 __proto__ 是 null。
但是之前说过,每一个原型都对应一个构造函数,所以会有一个构造函数 Object() ,而且构造函数本身也有原型,其原型是 Function.prototype。
最关键的地方来了,Function.prototype 也有原型,其原型是 Object.prototype,还没完,Function 自身也是一个构造函数,所以 其也有原型,而且原型是 Function.prototype。上面的 Object 也是一个构造函数,它的原型也是 Function.prototype 。
这里为什么关键呢?因为形成了一个看似矛盾的死循环。而这也是由函数出发构造实例和原型链的必然,就像 Python 中的 元类。在这里, Object 的原型不是Object.prototype,而是 Function.prototype ,也真是诡异至极。
Object 的 prototype 属性 当然是 Object.prototype,但 Object 的 __proto__ 是 Function.prototype,而 Function.prototype 没有 prototype 属性,只有 __proto__ ,而且是 Object.prototype 。
这说明 __proto__ 和 prototype 属性 在本质不同,且所有的构造函数的上一个原型,在「默认情况下」都是 Function.prototype 。
原生的构造函数及其原型链关系,轻易不能动,而我们自己定义的函数也好,构造函数也好,在默认情况下都是以 Function.prototype 为上一个原型。
这导致:
JS 中默认的原型链其实是很扁平的,并不像「链」所暗示的那样拥有「细长」的特质。用户通过人为变动自定义的构造函数的原型链关系,可以实现代码复用。也就是说,JS 中鼓励 抽象链条,而非「层次复杂的抽象层级」。
在以后可以慢慢加深理解,关键是把握好一个三角关系:
- 构造函数
- 构造函数的实例
- 构造函数的原型
和一个细微但重要的区分:
- prototype 属性(表面上的区分是,只有构造函数实质拥有)
- __proto__ 方法 (本质上是 Object 的方法,按照原型链特点,所有对象都拥有这个方法,除了 null)
可以举一个例子,String 是原生构造函数,我们来看下 String 相关的代码示例:
> String.prototype
[String: '']
> String.__proto__
[Function]
> String.hasOwnProperty('prototype')
true
> String.hasOwnProperty('__proto__')
false
> var mystr = '33'
undefined
> mystr.hasOwnProperty('prototype')
false
> mystr.hasOwnProperty('__proto__')
false
> mystr.prototype
undefined
> mystr.__proto__
[String: '']
> Object.getPrototypeOf(mystr)
[String: '']
>
以上可以清晰地看到 __proto__ 不是属性,而是一个方法属性,只是这么简要地写了,其实是一个包装。
JS 中的一切皆对象,最终逻辑落脚点是构造函数 Function, 而 Function 也是对象。Python 中的一切皆对象,最终逻辑落脚点是元类 type ,而 type 其实也是一个函数。
所以,这种抽象层面高于 C 的动态语言,其最终逻辑落脚点依然是「函数」?而这个函数构造了一个类似「指针结构体」的类型链条或者类型层级。
以上仅仅只是一个初步的猜测。不过了解了 C 、 C++ 、 Java 、 Go 以及 Scheme 之后,再加上原先的 Python 和 JS ,现在终于开始体会到编程的精微之处以及编程世界里需要以及可以掌握的无穷无尽的奥妙。
怪不得很多程序员略单纯呢,人的精力毕竟是有限的哇。而编程这门技艺,虽然的确精微,但也确实太过专精,甚至比很多科研领域还要狭窄,但同时也更有纵深。
不知是该恭喜,还是该怎样,总之阅读到该文的,你是第 人。每一次刷新,都是不同的自己。