JavaScript现在也有class与extends关键字,也可以像Java那样实现类与继承了,但从本质上讲,JS其实是一门原型语言,每个类型都有一个原型,每个类型在实例化时,都有一个原型属性指向了它的原型。JS用原型的概念保证了所有类型都具有相同的行为,这种朴素的思想特别利于初学者理解,JS是编程新人从自然语言跨向编程语言最容易的起点。
深入理解__proto__与prototype属性
JS是一门原型语言,JS中的一切皆为对象,每个对象都有一个原型(prototype),原型是对象实例在创建时,默认被继承的属性和方法的集合,原型对象中的属性、方法在对象实例中皆可访问。看一个示例代码:
1. let obj = {} 2. obj.toString() // Output:'[object Object]'
obj是一个使用对象字面量创建的对象实例,从字面上看,obj中并没有名为toString的方法,但第2行却可以调用,为什么?就是因为toString是原型对象上的方法。
在谈论原型这个概念时,涉及两个属性,即__proto__与prototype,这两个属性都表示原型,它们有什么分别呢?我们应当如何区分和理解它们呢?
在区分这两个概念之前,我们先理清另外两个概念:类型和实例。
什么是类型?例如Object、Function、Array、RegExp等,这些以大字字母开头(在其它语言中不一定是以大写开头的)的都是类型,包括我们在代码中自定义的class,也是类型,类型不是对象实例,是对象实例的模板。举个例子:
1. class Ball { ... } 2. let ball = new Ball()
其中,Ball便是类型,而ball是实例。
对于任何类型,都有一个prototype属性,例如Object.prototype、Function.prototype、Array.prototype等,prototype属性指向类型模板,模板中预定义了一些属性和方法,实例创建时,它所继承的属性和方法便是从[类型].prototype上复制的。JS作为一名原型语言,靠的便是这种机制实现了原型继承。
对于如下代码:
1. let arr = [1, 2, 3] 3. arr.length // Output:3 4. arr.push(4) 5. arr.pop() 6. arr.toString() // Output:'1,2,3'
变量arr的类型是Array,arr在创建时,从Array.prototype上复制了一些属性和方法(例如length、push、pop等),但toString方法并不是Array.prototype上的。toString方法在Object.prototype上,Array.prototype继承于Object.prototype,它们的继承关系是这样的:
JS中一切皆为对象,所有对象的原型最终都指向Object.prototype,这样一来,Object上面就没有对象了,所以Object.prototype的父级原型指向了null。
prototype是作为对象实例 中 继承的 原型对象 类型而存在的。 了解了prototype,再来看__proto_属性。
仍以数组实例arr为例,看一下它的结构:
1. arr:(3) [1, 2, 3] 2. 0: 1 3. 1: 2 4. 2: 3 5. length: 3 6. [[Prototype]]: Array(0) // Array.prototype 7. constructor: ƒ Array() 8. length: 0 9. pop: ƒ pop() 10. push: ƒ push() 11. ... 12. [[Prototype]]: Object // Object.prototype 13. constructor: ƒ Object() 14. toString: ƒ toString() 15. get __proto__: ƒ __proto__() 16. set __proto__: ƒ __proto__() 17. ...
注意:这个结构列表可以在浏览器或微信开发者工具的调试区查看。
在对象arr上,有一个[[Prototype]]属性(第6行),这个属性是一个对象,是以Array.prototype为模板复制的。在[[Prototype]]属性上,还有一个[[Prototype]]属性(第12行),这也是一个对象,它是以Object.prototype为模板进行复制的。第7行、第13行,每个[[Prototype]]属性上都有一个constructor成员,constructor是构建器函数,是使用new关键字创建实例时被调用的函数,这个constructor成员是在类型模板([类型].prototype)上定义的。
在最后这一级[[Prototype]]对象上,有一个名称为__proto__的getter和setter。 proto 是定义在Object.prototype上的存取器,既然JS中的一切皆为对象,那么一切实例都有__proto__这个存取器属性。__proto__作为存取器属性,在内部指向[[Prototype]],不过[[Prototype]]是不能在代码中直接访问的,只能通过__proto__访问。
综上所述, _proto _是实例的存取器属性,是对私有属性 [[Prototype]] 的封装,它返回实例上从原型模板([类型].prototype)上 复制 的实例属性。 如果基于__proto__描述继承关系,那么链条是这样的:
总结一下:
❑ __proto__是实例的存取器属性,封装内部对实例属性[[Prototype]]的访问,本质是存取器属性;
❑ prototype是作为对象实例在创建时继承的原型类型而存在的,本质是类型。
改变原型 的 指向便可以改变继承关系。 接下来看一个示例,进一步理解__proto__与prototype的区别,如代码清单6-10所示。
代码清单6-10通过改变原型改变继承
1.// JS:disc\第6章\6.2\6.2.3\change_prototype.js 2.class Being { 3. run(i) { 4. console.log(`${i} running..`) 5. } 6.} 7.// const Being = function () { 8.// this.run = (i) => { 9.// console.log(`${i} running..`) 10.// } 11.// } 12.class Person { 13. title = "微信小游戏" 14.} 15.// const Person = function () { 16.// this.title = "微信小游戏" 17.// } 18. 19.const person1 = { 20. print: function () { 21. console.log(`title:${this?.title}`) 22. } 23.} 24.person1.print() // Output:title:undefined 25.person1.__proto__ = new Person() 26.// Object.setPrototypeOf(person1, new Person()) 27.person1.print() // Output:title:微信小游戏 28. 29.person1.run?.(1) 30.person1.__proto__.__proto__ = new Being() 31.person1.run?.(2) // Output:2 running.. 32.new Person().run?.(3) 33. 34.Person.prototype.__proto__ = new Being() 35.person1.run?.(4) // Output:4 running.. 36.new Person().run?.(5) // Output:5 running..
这个文件做了什么事?
❑ 第2行至第6行声明了一个类型Being,声明效果与第7行至第11行相同。
❑ 第12行至第14行声明了另一个类型Person,声明效果与第15行至第17行相同。
❑ 第19行至至第23行,person1是一个对象实例,包含一个print方法成员。
❑ 第24行,person1默认是没有title属性的,所以这一行打印结果是“title:undefined”。
❑ 第25行,使用__proto__存取器将person1实例的原型设置为一个Person实例,必须是实例,不能是类型。第26行,使用静态方法setPrototypeOf与使用__proto__存取器的效果一样。在改变原型后,第27行的print打印后,便能取到title属性了。
❑ 第29行,此时person1实例上并没有run方法,这一行不会打印任何内容。
❑ 第30行,person1.__proto__指向Person实例,将它的__proto__设置为Being,相当于让Person继承于Being,如此一来,person1便拥有了run方法,所以第31行的run方法有输出。但第32行的run没有输出,因为第30行只是实例person1的原型改变了,新实例的原型没有改变。
❑ 第34行,Person.prototype是一个类型模板,它的 proto 本是undefined,将其设置为Being实例,也相当于让Person继承于Being。 改变后,第35行、第36行,无论是旧实例,还是新实例,都有run方法了。
最后总结一下,prototype作用在类型上, proto 作用在实例上,两者的赋值对象都必须是实例。无论使用这两个属性中的哪一个改变原型,继承关系都不是很清晰明朗,在实际开发中最简单明了的继承方法还是使用extends关键字,在类型声明时就确定了继承关系。
如何理解原型及原型链 ?
原型即prototype,当对象存在上下从属关系时,原型便形成了一个链条,这便是原型链。如果理解原型及原型链呢?
看一个示例代码:
1. // JS:disc\第6章\6.2\6.2.2\constructor.js 2. // 构造函数 3. function PersonConstructorFunction(name, age, job) { 4. this.name = name 5. this.age = age 6. this.job = job 7. this.friends = ["小王", "小李"] 8. this.say = function () { 9. return `我的名字是${this.name},我是一名${this.job}。` 10. } 11. } 12. let p = new PersonConstructorFunction("LY", 18, "程序员")
在这个示例中,第12行如果不使用new关键字,PersonConstructorFunction就是一个普通函数,它返回undefined;但是如果用了new,它就变成了一个构造器函数,this将指向创建后的实例。
对新创建的实例p,它的继承关系是:
prototype属性是类型属性,没有办法链式访问,但__proto__是实例属性,支持链式访问,对于上面的继承关系链,有如下链条:
p.__proto__.__proto__.__proto__ // 输出:null 复制代码
这个链条便是原型链,最后一个__proto__节点,指向Object.prototype的原型,是null。
如果我们再实例化出p2、p3,那么这些对象的原型继承关系如图6-5所示。
图6-5 p与p2、p3的原型关系图
用new PersonConstructorFunction()创建的对象还从原型上获得了一个constructor属性,它指向函数PersonConstructorFunction本身,示例代码如下,这些关系判断都会返回true:
1. // JS:disc\第6章\6.2\6.2.4\constructor.js 2. ... 3. console.log(p.constructor === PersonConstructorFunction.prototype.constructor) // Output:true 4. console.log(PersonConstructorFunction.prototype.constructor === PersonConstructorFunction) // Output:true 5. console.log(Object.getPrototypeOf(p) === PersonConstructorFunction.prototype) // Output:true 6. console.log(p instanceof PersonConstructorFunction) // Output:true
第5行,getPrototypeOf方法用于返回一个实例的原型。第6行,instanceof操作符用于判断左值是否为右值的一个实例。
执行如下指令对上面修改后的代码进行测试:
cd disc node ./第4章/4.2/constructor.js
输出:
true true true true
从测试可以看出,一个对象类型无论有多少实例,其原型均指向一处,原型是多个实例共享的一块内存区域。原型是类型,一个程序中会有许多实例,虽然每个实例都有原型,但因为原型是共享的,所以并不会因为原型链长而影响程序性能。
基于原型链实现万能的类型检测方法 instanceOf
在了解了原型及原型链的概念后,我们做一个练习:我们知道原生的instanceof操作符可以判断一个对象是否为某类型的实例,那么能否根据原型及原型链的概念自实现一个instanceOf函数,用其代替instanceof进行实例类型的判断呢?
答案是肯定的,示例代码如代码清单6-11所示。
代码清单6-11自定义instanceOf函数
1. // JS : disc\ 第 6 章 \6.2\6.2.5\instance_of.js 2. function instanceOf(target, kind) { 3. // basicTypes : "number", "boolean", "string", "undefined", "object" 4. switch (typeof target) { 5. case "number": { 6. return Object.prototype.toString.call(new kind) === "[object Number]" 7. break 8. } 9. case "boolean": { 10. return Object.prototype.toString.call(new kind) === "[object Boolean]" 11. break 12. } 13. case "string": { 14. return Object.prototype.toString.call(new kind) === "[object String]" 15. break 16. } 17. case "undefined": { 18. return Object.prototype.toString.call(kind) === "[object Undefined]" 19. break 20. } 21. case "object": 22. default: { 23. // 有 typeof 为 null 的情况, toString 结果为 [object Null] 24. if (!!!target && Object.prototype.toString.call(kind) === "[object Null]") return true 25. const left = target.__proto__ 26. , right = kind.prototype 27. if (left === null) { 28. return false 29. } else if (left === right) { 30. return true 31. } else { 32. return instanceOf(left, kind) 33. } 34. } 35. } 36. } 37. // 测试代码 38. console.log(instanceOf(0, Number)) // Output : true 39. console.log(instanceOf("0", String)) // Output : true 40. console.log(instanceOf(true, Boolean)) // Output : true 41. console.log(instanceOf(null, null)) // Output : true 42. console.log(instanceOf(undefined, undefined)) // Output : true 43. console.log(instanceOf(Symbol("s"), Symbol)) // Output : true 44. console.log(instanceOf({}, Object)) // Output : true 45. console.log(instanceOf(/.{2}/, RegExp)) // Output : true 46. class Class1 { } 47. class Class2 extends Class1 { } 48. class Class3 extends Class2 { } 49. console.log(instanceOf(new Class2, Class1)) // Output : true 50. console.log(instanceOf(new Class3, Class1)) // Output : true 51. console.log(instanceOf(new Class3, RegExp)) // Output : false
这个instanceOf函数是怎么实现的?
❑ 第25行至第33行是关键代码。第25行取出实例原型,第26行取出类型原型,如果它们全等,在第30行返回true;如果不相等,将left作为检测目标,在第32行递归调用instanceOf,这是沿着原型链向上走;如果走到了Object.prototype,此时left为null,代表原型链走到了尽头仍然没有匹配,在第28行返回false。
❑ 第4行至第24行处理的是基本类型检测的特殊情况。加上这些代码,我们自定义的instanceOf方法不仅可以检测对象,还可以检测基本数据类型的变量,包括null、undefined等。
❑ 第24行,当typeof target为object时,有可能是null,这是特殊情况。
❑ 第38行至第51行是测试代码。
从测试结果来看,instanceOf满足要求,支持对象及所有基本类型的测试。
自定义实例化函数,以代替new关键字
JS是一门原型语言,class其实是基于原型的语法糖,当使用class定义了一个类型,并用new关键字实例化一个对象时,其实是先创建一个实例,然后将类型的原型给它拷贝一份,实例化就是这么简单。在深入理解了JS的原型概念以后,我们甚至可以自实现一个实例化函数,以代替new关键字。
第六章第18课有这样一道思考题:
思考与练习6-1(面试题):试尝试实现一个函数,代替new关键字,实现基于构建函数创建对象的逻辑。
下面是这个问题的参考答案(附录思考与练习6-1)
构建函数前面的new关键字主要完成了3件事:
❑ 基于构建函数的prototype原型属性,创建一个新对象;
❑ 将新对象绑定为构建函数中的this,并执行构建函数;
❑ 返回对象,如果构建函数有返回并且是引用对象,返回它;否则返回创建的新对象。
针对这个逻辑,代替new关键字的newOperator函数如代码清单1-6所示:
代码清单1-6实现newOperator函数
1. // JS : disc\ 第 6 章 \6.2\6.2.2\new_operator.js 1. function newOperator() { 2. const constructor = Array.prototype.shift.call(arguments) 3. 4. // 判断参数是否是一个函数 5. if (typeof constructor !== "function") throw SyntaxError("第1个参数须是函数") 6. 7. // 创建一个空对象 8. const newObject = Object.create(constructor.prototype) 9. // 绑定this对象,执行构建函数 10. const result = constructor.apply(newObject, arguments) 11. // 看构建函数是否有返回并且是引用类型 12. const flag = result && (typeof result === "object" || typeof result === "function") 13. 14. // 返回结果 15. return flag ? result : newObject 16. } 17. 18. // 构造函数 19. function PersonConstructorFunction(name, age, job) { 20. this.name = name 21. this.age = age 22. this.job = job 23. this.friends = ["小王", "小李"] 24. this.say = function () { 25. return `我的名字是${this.name},我是${this.job}。` 26. } 27. } 28. // let p = new PersonConstructorFunction(" LIYI ", 18, "程序员") 29. let p = newOperator(PersonConstructorFunction, "LIYI", 18, "程序员") 30. p.say() // 我的名字是 LIYI ,我是程序员。
在该示例中:
❑ 第2行至第17行是newOperator函数。
❑ 第3行,将第一个参数取出作为构建函数,newOperator的第一个实参必须为函数。
❑ 第9行,使用Object.create基于一个原型类型,创建一个对象,就是以此对象作为新对象实例的[[Prototype]]属性。
❑ 第11行,在调用apply方法时,将新对象实例newObject作为构建函数中的this绑定了。
❑ 第13行,检测构建函数有没有返回,准备返回创建的对象实例。
第20行至第31行是测试代码,从测试结果看,使用new关键字的第29行代码与使用newOperator函数的第30行代码,效果是一样的。
小结
JS作为一门原型语言,所有类型都有一个原型且只有一个原型,当类型实例化时,原型被拷贝一份,实例属性装进实例里,存取器属性和方法映射向原型对象,这便最大程度节省了程序在运行时占用的内存。
如果想通过原型改变对象之间的继承,使用prototype属性作用在类型上,改变的是整个类型的定义,使用__proto__实例作用在实例上,改变的是指定实例继承的属性和行为。
由于JS是一门动态语言,且原型允许在运行时动态修改,所以如果未来JS拥有独立意识的话,它可能是电子计算机世界里最先进化出独立智能的一门语言。它可以通过不断优化自身而越变越强大,由于这个过程不需要重新编译与重新启动,它可以悄悄地不断进行,以指数级的速度迅速蜕变,一夜震惊整个世界。