首先,此文是对于javascript原型链的一些私人见解,若能博君会心一笑,在下荣幸之至!
为了阐述我的理解,首先提前声明一些前置知识,欢迎指正:
栈内存和堆内存:
栈内存每个地址分配的地址长度较窄,且长度固定,用于检索和快速遍历,一般存着值类型数据,如string,number,booleen,null。
堆内存的每个地址分配的地址长度是弹性变化的,用于存储具有一定结构的数据,如函数对象function,实例对象object等,为了提高检索效率,所有堆内存里对象的存储地址都记录在栈内存内,这样通过栈内存遍历就可以快速定位堆内存内的目标。
在原型链里,常有这种情况:多个栈内存地址__proto__指向另一个栈内存地址prototype,而prototype指向堆内存内某一对象地址。
重点1 -> prototype和__proto__(隐式属性):
对象分为函数对象和实例对象,函数对象又分为构造函数对象和普通函数对象。
首先所有对象都具有__proto__属性,而特别地,函数对象内一定有prototype属性,打印任何对象时浏览器内默认不显示隐式属性,
所有函数都有prototype这一属性对象(原型),且存在constructor属性存储自身堆内存所在地址,即存在隐式属性prototype:{constructor:f}。
重点2 -> 自有属性和继承属性(显式属性和隐式属性):
var a = {}
console.log( a.b ) // undefined
a.__proto__ = { b:'b' }
console.log( a.b ) // b
函数对象一般有两个隐式属性,prototype指向自己的原型对象,__proto__一般会指向另一个函数对象的原型对象。
从第三行代码看出对象的隐式属性是可以随意修改的,这也是javascript的一大特点,第四行代码实际上查找的是a.__proto__.b,浏览器隐藏了这一过程。
访问函数的属性时,存在一个两遍查找的过程,第一遍查找自有属性,如果自有属性没有去继承属性__proto__里查找,__proto__会指向另一个函数对象的原型,在此原型里找同名属性,那如果没有找到怎么办呢?
我们上文说过,函数对象有两个隐式属性,既然自己的prototype指向的这个原型对象里没有它需要找的同名属性,就会通过自己的另一个隐式属性__proto__再指向其构造函数的prototype,再到prototype指向的原型对象里去查找,由此形成原型链,事实上,原型链在进入到最后一环之前一直是以这样一种简单的规则勾连的。
重点3 -> 继承本质:
var a = function () {} // #不一定要是构造函数
var b = new a();
console.log( b.__proto__ === a.prototype ) // true #类型和存储的地址都相同
所有被函数构造出来的实例对象,其隐式属性__proto__指向其构造函数的隐式属性prototype。
如果一个构造函数构造了许多实例对象,它们所有的__proto__相当于前文提到的栈内存入口,指向同一个构造函数的prototye,而构造函数的prototype是第一个指向其原型对象的栈内存入口,一般也可以将prototype说成原型对象,当然本质是其指向堆内存里的对象地址。
堆内存里的原型对象所具有的属性和方法,javascript通过一些底层方法使得实例对象省略.__proto__的形式直接访问,且__proto__一旦找到对应的属性或方法就返回结果,不再往上寻找更高一级的原型对象,这就是继承的本质。
顶级构造函数Object 和 Function:
Function.prototype.hasOwnProperty = function () { return 'Function prototype' } // #Object的原型里有内嵌hasOwnProperty方法
function f () { this.x = 1 }
f.x = 1
var obj = new f()
console.log( f.hasOwnProperty( 'x' )) // Function prototype
console.log(obj.hasOwnProperty( 'x' )) // true
console.log( Object.hasOwnProperty( 'x' )) // Function prototype
console.log( Object.__proto__ === Object.prototype ) // false
console.log( Object.__proto__ === Function.prototype ) // # Object将会继承Function的原型!
console.log( Function.prototype === Function.__proto__ ) // true #这意味着Function会自己继承自己的原型!
console.log( Function.hasOwnProperty( 'x' )) // Function prototype
上文说到如果自己的原型对象里没有要查找的同名属性,则通过另一个隐式属性__proto__再指向另一个函数的prototype,如此循环往复,最后都会到达两个顶级的构造函数,此时顶级函数Object的隐式属性__proto__将会找到Function的原型对象,令人惊讶的是Function的__proto__指向自身的prototye,这就意味着Function会继承自己的原型对象!
对于所有实例对象和衍生函数而言,Object和Function是原型链的终点,它们两之间的继承关系显得特别混乱,如果按原型链的规则来制定最后一级的继承关系,势必引起所有函数和对象逻辑上的混乱。
事实上,为了让函数具备对象的特性,在逻辑上是不可能有一套通用的规则的。
我们假设javascript这种语言不再赋予函数对象的特性,那么原型链不仅会变得特别好理解,而且层次分明。
Object和Function两个构造函数可以各司其职,各不相关,一个负责构造对象,一个负责构造函数,多么美妙而有序。所有对象的顶级原型都是Object.prototype,所有函数的顶级原型都是Function.prototype,然而javascript作为一门混沌而具有无穷潜力的语言,善于搜刮各种其他语言的优势填补自身的短板,而其他热门语言所具有的一大杀器便是继承的特性,对象和继承使javascript衍生出另一项绝活------构造函数。
构造函数使得构造对象不再是Object函数的专利,但这也引申出另一个问题,那就是原型链的顶级原型和下一级原型之间需要谨慎应对(毕竟使用隐式属性模仿继承总有漏洞,需要一些手段来矫正规则),于是函数和实例对象的原型链在进入到最后一级的原型后,javascript在底层对它们添加了‘新的继承规则’。
这些添加的规则使得所有实例对象和函数的继承特性在最后一级也表现得富有逻辑,比如一个实例对象,它既然不是函数,就不应该继承任何Function的原型属性和方法,因而Function的原型不会对任何实例对象造成干扰。
一个函数,它若即是函数又是对象,在面向对象编程思想盛行的时代,就有继承Object所有方法和属性的权利,于是在函数的顶级原型Function.prototype上又加了一层Object.protype这一原型。
Javascript通过隐式属性和在最后一级‘新的继承规则’成功复制了继承这一经典模式,除此之外,为了填补自身短板,适应各类编程思想,Javascript还发展出许多新的特性。
比如摆脱弥补饱受诟病的弱类型特点,Javascript发展出strict模式下的编程,增加了许多报警机制,甚至衍生出一门新的语言Typescript。为了向Java等强类型语言致敬,发展出访问器属性,这一机制也是前端框架的核心原理之一.。
最后,希望你我都能在Javascript语言中找到编程的乐趣,以上拙见,转载请注明出处。。