原型继承
编程中希望能扩展或复用一些东西,JS中的原型继承这个语言特性可以实现这一个需求。
[[Property]]
一个内部隐藏属性,要么是 null(Object.prototype.proto或Object.create(null)) ,要么是另一个对象的引用。
设置原型和获取原型的方式
使用 __proto__ 来获取,或设置。
let animal = {eats: true}let rabbit = {jumps: true}rabbit.__proto__ = animalconsole.log(rabbit.__proto__) // animal
效果
访问rabbit上不存在的属性或方法,会顺着其原型对象找到animal身上。这里我们说animal是rabbit的原型。
原型链可以很长。
本质
proto是[[Prototype]] 的因历史原因而留下来的 getter/setter。现代语言建议使用 Object.getPropertyOf(obj) & Object.setPropertyOf(obj) 取代使用 __proto__ ,但规范要求支持 __proto__ 因此使用它是安全的。
注意:
- 不能在闭环中分配 **
__proto__** - proto只能设置对象和null,其他会忽略
读取属性使用原型,写入属性不使用原型
写入和删除,都是直接在操作的对象本身进行的。
let animal = {walk() {console.log('animal walk')}}let rabbit = {__proto__: animal}rabbit.walk = function(){console.log('rabbit walk')}rabbit.walk() // rabbit walk
但访问器属性是例外(因为本质是函数啊!所以实际上是调用了set函数)
let user = {name: "John",surname: "Smith",set fullName(value) {[this.name, this.surname] = value.split(" ");},get fullName() {return `${this.name} ${this.surname}`;}};let admin = {__proto__: user,isAdmin: true};alert(admin.fullName); // John Smith (*)// setter triggers!admin.fullName = "Alice Cooper"; // 这里调用了user的set fullNamealert(admin.fullName);alert(user.fullName);
this的指向
上例中的this,指向的是user还是admin呢?
注意,this不受原型影响,只关注this前的符号.
let animal = {walk() {console.log('animal walk')}eat() {this.eating = true}}let rabbit = {__proto__: animal}rabbit.eat() // rabbit.eating = true animal.eating = undefined
for in
遍历对象属性,也会迭代继承的属性,如果属性是不可迭代(enumerable: false),则无法读取。
如果只想获取对象自身的属性,可以通过 getOwnProperty 来进行判断。
几乎所有其他的键/值获取方法都忽略继承的属性,比如Object.keys | values等。
练习
注意查找还是赋值
let hamster = {stomach: [],eat(food) {this.stomach.push(food); // *}};let speedy = {__proto__: hamster};let lazy = {__proto__: hamster};// 这只仓鼠找到了食物speedy.eat("apple");alert( speedy.stomach ); // apple// 这只仓鼠也找到了食物,为什么?请修复它。alert( lazy.stomach ); // apple
*号这里要注意, this.stomach.push 会沿着原型链去查找 stomach 属性,而不是给this对象去赋值。因此题目里会沿着原型链查找到 hamster 身上。
F.prototype
通过 new F() 调用构造函数可以创建一个新的对象。如果 F.prototype 是一个对象,则 new 操作符会使用它为新的对象(实例)设置 [[Prototype]]
JS从语言设计之初就有原型继承,但过去缺少访问它的方式,唯一可靠的方式是访问,构造函数的** **
prototype属性。
只是常规(普通)属性
从单词字面理解,跟原型很像,但实际上只是一个常规的属性,可以修改。
let animal = {eats: true};function Rabbit(name) {this.name = name;}Rabbit.prototype = animal // 在声明这句话前,Rabbit其实已经有了prototype属性,是一个对象,具有constructor属性,指向构造函数自身let rabbit = new Rabbit('white'); // 此时构造出的实例,原型指向的是animal对象了。rabbit.eats // trueA

- F.prototype仅仅在函数作为构造函数调用,即new F()时,才会使用它,将其作为实例对象的[[prototype]]的值。
- F.prototype可以随时更改,更改后创建的实例拥有最新的prototype属性值作为其[[prototype]]的值,而之前创建的实例,[[prototype]]保持原有旧值。
默认的prototype
每个函数在声明后,都会有一个 prototype 属性,默认是一个对象,包含 constructor 属性,指向函数自身。
function Rabbit() {}/* default prototypeRabbit.prototype = { constructor: Rabbit };*/

通常不作修改原型的操作,我们可以判断实例来自谁
let rabbit = new Rabbit() // prototype = {constructor: Rabbit}rabbit.constructor == Rabbit // true,访问的是其原型上的constructor// 所以我们还可以这么干let rabbit2 = new rabbit.construtor()
JS不能确保正确的constructor值
因为我们可以覆盖式重写 prototype
function Rabbit() {}Rabbit.prototype = {jumps: true};let rabbit = new Rabbit();alert(rabbit.constructor === Rabbit); // false
所以正确的写法应该是添加和删除,而不是覆盖
function Rabbit() {}// 不要将 Rabbit.prototype 整个覆盖// 可以向其中添加内容Rabbit.prototype.jumps = true
原生的原型
Object.prototype
let obj = {}alert(obj) // [object Object]// 这里之所以生成了字符串,是因为obj.prototype 指向的是内置Object的prototype对象,其包含很多方法,toString就是其中之一。

obj.__proto__ === Object.prototype
注意:Object.prototype之上没有更多的原型了。
Object.prototyoe.__proto__ === null
其他内建原型
像Array,Date,Function等等,都是在prototype上挂在了很多方法。
let arr = []arr.__proto__ === Array.prototypeArray.prototype.__proto__ === Object.prototypeObject.prototype.__proto__ === null// 所以arr.__proto__.__proto__.__proto__ === null
注意:一些方法在原型上会有重叠,比如Array.prototype有自己的 **toString** 方法,这种情况,使用原型链查找中最近的方法。
let arr = [1,2,3]arr.toString() // 1,2,3 调用Array.prototype.toString方法// 看下如果使用了Object.prototype.toStringObject.prototype.toString.call(arr) // "[object Array]"
内置原生原型可修改
String.prototype.show = function() {alert(this);};"BOOM!".show(); // BOOM!
因此很容易覆盖,冲突。现代编程中,只有polyfilling才允许修改原型。
原型借用
一种灵活的技巧。
let obj = {0: "Hello",1: "world!",length: 2,};// 因为数组方法要求不高,只要符合数字索引,有length,就可以像数组一样被使用obj.join = Array.prototype.joinobj.join(',') // Hello,world
题目
给函数添加一个f.defer(ms)方法
function f() {alert("Hello!");}f.defer(1000); // 1 秒后显示 "Hello!"Function.prototype.defer = function(ms) {const fn = thissetTimeout(fn, ms)}
装饰器defer
function f(a, b) {alert( a + b );}f.defer(1000)(1, 2); // 1 秒后显示 3Function.prototype.defer = function (ms) {const fn = this; // 拿到函数f,因为是f.defer,.前面就是this指向了,这里f是个函数,也就是我们的题目中的原始的需要真正执行的函数。return (...args) => {setTimeout(fn.bind(this, ...args), ms); // *};};
注意星号的this
原以为:这里fn.bind(this 中的this,可以保证适用于对象方法来调用。该题中,此时this = window,因为是f.defer(ms)得到了这个return的函数,这个函数是裸调用,因此是window。
~~
这里return的是箭头函数,因此没有自己的this,获取外层的,应该是f.defer(ms)(1,2)调用,因此是f,而且是bind绑定,this的优先级较高(下面变体解释会用到)
但如果将箭头函数改为普通匿名函数,则指向this。
Function.prototype.defer = function (ms) {const fn = this;return function (...args) {console.log(this);setTimeout(fn.bind(this, ...args), ms);};};f.defer(1000)(1, 2)// f.defer(ms) 拿到return的函数,然后裸调用,指向window。
如果需要适应对象方法调用,箭头函数的写法就有问题
Function.prototype.defer = function (ms) {const fn = this;return (...args) => {console.log(this)setTimeout(fn.bind(this, ...args), ms); // *};};let user = {name: "John",sayHi() {alert(this.name);}}user.sayHi = user.sayHi.defer(1000)user.sayHi() // 这里看起来是对象.方法调用,this应该是user,打印John。但!!!不是这样的,注意看星号代码,通过bind绑定的this,且箭头函数无this,获取外层的,user.sayHi.defer,看defer前的对象,是sayHi,因此这里bind的this是sayHi。打印看看结果: sayHi因为alert调用了函数自身的toString(),打印了函数自身
原型方法,无proto的对象
__proto__ 是以前为了方便获取和设置原型而存在的一个 setter,getter访问器属性,JS规范并不推荐,建议使用如下:
- Object.create(proto, [descriptors]),第二个参数是可选的属性描述。
- Object.setPrototypeOf(obj, proto)
- Object.getPrototypeOf(obj)
一种强大的拷贝方式:
const clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))
原型历史
- 函数的
prototype一直都存在,从声明开始就有。 - 12年,
Object.create出现在标准里,但仅能创建,没有提供set/get的能力,浏览器厂商自己实现了非标准的__proto__访问器,允许用户随时get/set - 15年,
Object.setPrototype|getPrototypeOf加入到标准,但__proto__已经加入到JS标准附件中,在所有环境基本上都是支持的。使用标准,而非proto
因为proto其实是访问器属性,setter内部做了判断,仅支持null和对象,不支持字符串等,所以赋值无效,这很容易出现隐藏BUG,难以发现。 ```javascript let obj = {};
let key = prompt(“What’s the key?”, “proto“); obj[key] = “some value”;
alert(obj[key]); // [object Object],并不是 “some value”!
1. 你可以采用 `Map` 来代替普通对象存储1. 使用纯字典|标准对象 `Object.create(null)` ,创建一个无原型对象。<a name="ap0ZL"></a>#### **注意:**上述第二个方法也有一点小缺点,无原型,说明无原型上的方法可供使用,如toString。```javascriptlet obj = Object.create(null)obj.toString() // obj.toString is not a function
但对象的大部分实用方法,还是在Object对象上的,可以继续使用
Object.keys(obj) // []
练习
- 添加原型方法,使其工作 ```javascript let dictionary = Object.create(null);
// 你的添加 dictionary.toString 方法的代码
// 添加一些数据 dictionary.apple = “Apple”; dictionary.proto = “test”; // 这里 proto 是一个常规的属性键
// 在循环中只有 apple 和 proto for(let key in dictionary) { alert(key); // “apple”, then “proto“ }
// 你的 toString 方法在发挥作用 alert(dictionary); // “apple,proto“
```javascriptlet dictionary = Object.create(null, {toString: {// 其他标识符默认为false,不用管。// 回顾下,如果是对象字面量形式书写,默认值都是true了。value() {return Object.keys(this).join()}}})
