自己关于js的对象和原型链的一些理解和随笔
对象的概念
JavaScript 中,万物皆对象!但对象也是有区别的。分为普通对象和函数对象,Object 、Function 是 JS 自带的函数对象。下面举例说明
let o1 = {} let o2 =new Object() let o3 = new f1() function f1(){} var f2 = function(){} var f3 = new Function('str','console.log(str)') console.log(typeof Object) //function console.log(typeof Function) //function console.log(typeof f1) //function console.log(typeof f2) //function console.log(typeof f3) //function console.log(typeof o1) //object console.log(typeof o2) //object console.log(typeof o3) //object
在上面的例子中 o1 o2 o3 为普通对象,f1 f2 f3 为函数对象。怎么区分,其实很简单,凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象。f1,f2,归根结底都是通过 new Function()的方式进行创建的。Function Object 也都是通过 New Function()创建的。
关于__proto__
一般我们平常创建的普通对象,其原型都是继承Object这个对象的,例如下面这样:
let p0 = {<br />test1: 89,<br />test2: 109<br />}
console.log(p0.__proto__)
__proto__储存的是这个对象的隐式原型,可以将其理解为对象继承的原型源。像上面的p0对象继承的隐式原型就是Object对象。而如果再使用Object.create创建一个继承p0对象的对象,那它的__proto__就是p0,例如下面这样:
let p0 = { test1: 89, test2: 109 } console.log(p0.__proto__) let p1 = Object.create(p0) console.log(p1.__proto__) // { test1: 89, test2: 109, __proto__: Object }
这时你会发现你使用p1.test1可以调到p0对象的test1,因为这时p1对象已经继承p0的数据了。这时你可能想试试改变p1.test1的值,不过你如果用p1.test1 = 111 这种方式赋值,会发现用p1.test1调到的值确实是111,不过p0的值还是89,而你打印p1会发现值为111的test1键名属于p1,而p1.__proto__里的test1还是属于89。这是因为如果没有通过调用__proto__改变对象值,那改变的值都是在对象本身里面,如果你用p1.__proto__.test1 = 1100,就会发现p0的test1已经变为1100,不过你调用p1.test1的值依然为111,这是因为如果继承链上如果有相同的键名,那就会调用离该对象最近的键名。如果你想用p1对象访问p0的test1,那你需要这样调用才行:p1.__proto__.test1
let p0 = { test1: 89, test2: 109 } console.log(p0.__proto__) let p1 = Object.create(p0) console.log(p1.__proto__) // { test1: 89, test2: 109, __proto__: Object } p1.test1 = 111 console.log(p1.test1) // 111 console.log(p0.test1) // 89 p1.__proto__.test1 = 1100 console.log(p1.test1) // 111 console.log(p1.__proto__.test1) // 1100 console.log(p0.test1) // 1100
当然,一般不提倡通过这种办法在其他对象上修改另一个对象的值,最好是直接用p0.test1 = 1100在p0对象上修改,这样代码会比较清楚而且这样修改的话p1对象也可以通过p1.test1调到p0的值(前提是p1对象上没有名为test1的键名,如果有就必须用p1.__proto__.test1才能调到p0的test1的值)
继承链
刚才我们有提到一个词“继承链”,何为继承链?可能已经有人注意到打印p1.__proto__时,它底下还有一个__proto__,而这个在更里层的__proto__储存的是一个Object对象,没错,它就是p0的__proto__。只要一个对象继承了另一个对象,那它也会继承到那个对象的__proto__,而一个新对象再继承了它,那新对象的__proto__会指向的它,而它的__proto__也会给予新对象,这样一层层嵌套,直到最初的对象的__proto__,例如下面这样:
let p0 = { test1: 89, test2: 109 } console.log(p0.__proto__) let p1 = Object.create(p0) console.log(p1.__proto__) // { test1: 89, test2: 109, __proto__: Object } console.log(p1.__proto__.__proto__) // Object对象 p1.f1 = 'aa' p1.f2 = 'bb' p1.f3 = 'cc' let p2 = Object.create(p1) console.log(p2.__proto__) // { f1: 'aa', f2: 'bb', f3: 'cc', __proto__: Object } console.log(p2.__proto__.__proto__) // { test1: 89, test2: 109, __proto__: Object } console.log(p2.__proto__.__proto__.__proto__) // Object对象
我们发现p2的__proto__指向p1,而__proto__底下还有一个__proto__指向p0,更底下就是Object对象了,Object对象底下就没有__proto__了,一般遇到Object对象可以说继承链已经到底了。而p2可以同时访问整个继承链下的数值,例如通过p2.f1就能访问到p1的f1,通过p2.test1就能访问到p0的test1,当然,这些的前提是整个继承链上没有同名的键名,如果有同名的键名,那它访问的就是离它最近的键名。
关于函数对象
上面我们说的都是普通对象,现在讲讲函数对象。函数对象是这样定义的:
function test () { this.test1 = 123 this.test2 = 234 } let demo = new test() console.log(demo) // { test1: 123, test2: 234, __proto__: Object } console.log(demo.__proto__) // { constructor: test(), __proto__: Object对象 }
这时会发现demo对象里出现了test1和test2的数值,这是函数test执行的结果,当你new一个函数时相当于执行它的代码,而这一步执行的函数就是demo的构造函数constructor,demo对象会将这个函数储存在__proto__里面当成构造函数。如果改变了demo对象的test1或者test2想要变回来,可以通过constructor函数重新赋值给test1和test2,如下面这样
function test () { this.test1 = 123 this.test2 = 234 } let demo = new test() console.log(demo.test1) // demo.test1 = 333 demo.test3 = 999 console.log(demo) // { test1: 333, test2: 234, test3: 999, __proto__: Object } demo.constructor() console.log(demo) // { test1: 123, test2: 234, test3: 999, __proto__: Object }
demo的test1先是被改为333,同时又添加了值为999的test3,然后执行构造函数,test1又被重新赋值为123,test3因为没有在构造函数里使用,所以依然保留着本来的数值。
构造函数constructor也是会被继承的,如下面这样
function test () { this.test1 = 123 this.test2 = 234 } let demo = new test() let demo1 = Object.create(demo) console.log(demo1) // { __proto__: Object } console.log(demo1.__proto__) // { test1: 123, test2: 234, constructor: test() } demo1.constructor() console.log(demo1) // { test1: 123, test2: 234, __proto__: Object }
因为demo1没有添加数据,因此只有__proto__对象,而__proto__又包含demo的构造函数,demo1可以使用demo的构造函数给自己添加数据。
函数对象的prototype
函数对象还有一个prototype属性,有点函数对象的隐式原型__proto__的意味。你如果在prototype属性上定义数据,如果prototype属性上的值改变的话,所有继承到该函数对象的对象,它的继承链上的数值都会变,看看下面的例子:
function test () { this.test1 = 123 this.test2 = 234 } test.prototype.p1 = 'p1' let demo = new test() console.log(demo.__proto__) // { p1: 'p1', constructor: test(), __proto__: Object对象 } let demo1 = Object.create(demo) console.log(demo1.__proto__.__proto__) // { p1: 'p1', constructor: test(), __proto__: Object对象 } let demo2 = new test() console.log(demo2.__proto__) // { p1: 'p1', constructor: test(), __proto__: Object对象 } test.prototype.p1 = 'p' test.prototype.p2 = 'p2' console.log(demo.__proto__) // { p1: 'p', p2: 'p2', constructor: test(), __proto__: Object对象 } console.log(demo1.__proto__.__proto__) // { p1: 'p', p2: 'p2', constructor: test(), __proto__: Object对象 } console.log(demo2.__proto__) // { p1: 'p', p2: 'p2', constructor: test(), __proto__: Object对象 }
可以看到所有有test函数的继承链上的数据也跟着变化了,即使是后面才声明的p2也会被加进demo、demo1、demo2对象中,只要test函数的prototype属性有变的话,所以有它的继承链的对象也会跟着变。