你在编写代码时,经常会使用 const obj = {}; 定义一个对象,function定义一个函数,在这些代码运行完成时发生了什么呢?
F12打开控制台 操作:
一、对象
1、输入const obj = {};然后按enter。这时定义了一个对象obj
2、再输入obj按enter。控制台会显示obj对象的结构信息,根部是[[Prototype]]:Object,属性有constructor, hasOwnProperty,...,__proto__等等。
3、你再输入obj.__proto__ 控制台输出constructor, hasOwnProperty,...,__proto__,
4、输入Object.prototype.__proto__,输出null。
仔细一看不同的是obj输出的__proto__属性值不是null,而obj.__proto__输出的__proto__是null。
💡可以得到的结论是:每个对象天生就有__proto__。obj.__proto__ = Object.Prototype
Object.prototype.__proto__ 是 null,这就是原型链的终点。
注:控制台输入obj.prototype,却打印undefined,说明什么?对象是没有prototype属性的
二、函数
1、输入 function Chef(name) { this.name = name; } 按enter。
2、输入Chef.prototype 输出一个{},含有constructor属性。可见prototype是函数的属性,当然万物皆对象,这里可以看到还有个[[Prototype]]:Object。普通对象的prototype是null
3、输入Chef.prototype.constructor 输出fChef(name){this.name name;} 。你发现Chef.prototype.constructor === Chef
💡由此,我们得出结论:每个函数自带一个prototype属性,它是一个空对象,这个空对象{}里自动有一个constructor属性,指回这个函数。可以理解为prototype是函数的“备份本”,constructor 是备份本的“签名”。
三、挂载
概念清楚了,现在给函数Chef的prototype上挂载2个方法:
Chef.prototype.cook = function(food){ return `${this.name}正在烹饪${food}`; } Chef.prototype.introduce = function(){ return `我是 ${this.name}`; }然后再在控制台输入Chef.prototype 这时候会输出的对象就不是空对象了,而是:
{cook:f,introduce:f},
💡得出结论:cook 和 introduce 被挂到了 Chef.prototype 上,它们在内存中只存一份,未来所有 Chef 实例都共享
四、实例
用new创建一个实例 const chef1 = new Chef('王师傅'); 这个代码运行后发生了什么呢?拆解如下:
- 创建一个空对象 {}
- 让空对象的__proto__指向 Chef.prototype
- 用空对象作为this,执行 Chef函数体 即this.name = "王师傅"
- 返回这个空对象
再在控制台输入chef1 按下enter后输出 chef1对象 Chef{name:'王师傅'},展开可以看到[[Prototype]]:Object上挂载了cook和introduce方法
输入chef1.__proto__ 输出{cook:f,introduce:f}可见 chef1.__proto__ === Chef.prototype
输入chef1.constructor 输出fChef(name){this.name name;}可见 chef1.constructor === Chef
💡得出结论:chef1.__proto__ 指向 Chef.prototype(这就是"手指"),chef1.constructor 能访问到,是因为沿着 __proto__ 向上找到了 constructor'
再创建一个“李师傅” const chef2 = new Chef("李师傅");对比chef1和chef2。我们可以得出结论:
💡 chef1 和 chef2 的 __proto__ 都指向同一个 Chef.prototype
💡 所以 chef1.cook === chef2.cook === Chef.prototype.cook(同一个函数!)
五、调用
输入chef1.cook('鱼') 则输出:'王师傅正在烹饪鱼'
chef1自身没有cook,chef1.__proto__有cook,就是Chef.prototype.cook, 执行这个函数this = chef1,所以this.name 是 ‘王师傅’
输入chef2.cook(‘鱼’) 则输出:'王师傅正在烹饪鱼'
那么,我们修改Chef.prototype.cook后,再调用chef1.cook('鱼')和chef2.cook(‘鱼’)时会怎样?
Chef.prototype.cook = function(food) { return `${this.name} 正在用【新配方】烹饪 ${food}`; }; // 修改 prototype,所有实例立刻更新 chef1.cook('鱼') //王师傅正在用【新配方】烹饪鱼 chef2.cook("鱼") //李师傅正在用【新配方】烹饪鱼💡 因为 chef1 和 chef2 的手指都指向同一本秘方本
💡 秘方本上的内容改了,所有徒弟立刻就看到了新版本
六、实例自身属性 vs 原型属性
我们以上讨论的 cook 和 introduce 都是原型属性。那chef1 和chef2 可以有自身属性吗?
我们给chef1挂一个自身属性dishes: chef1.dishes = [‘红烧肉’]; chef1.push('麻婆豆腐');
请问 name,dishes,cook这三个哪些是自身属性,哪些是原型属性?
- chef1.hasOwnProperty('dishes')); // true(自身属性)
- chef1.hasOwnProperty('name') // true 自身属性
- chef1.hasOwnProperty('cook') // false(原型属性)
dishes好理解,但为啥name是自身属性呢,原因出在 this
// 原来的写法(原型属性) function Chef(name) { this.name = name; } Chef.prototype.cook = function(food) { ... };那如果每个实例的cook也是不同的,每个徒弟按照秘方制作米饭的时候,米可以有多种选择呢?
// 当你需要 每个实例有不同的函数行为 时,比如闭包场景: function Chef(name, type) { this.name = name; this.cook = function(food) { // type 是闭包变量,每个实例值不同 return `${this.name} 用${type}烹饪 ${food}`; }; }const chef1 = new Chef('李师傅', '黑米');
输入 chef1.cook('黄焖鸡’) 输出 :李师傅用黑米烹饪黄焖鸡
const chef2 = new Chef('王师傅', '白米');
输入 chef2.cook('黄焖鸡’) 输出 :王师傅用白米烹饪黄焖鸡
总结:能放原型就放原型,只有需要差异化行为时才放自身
七、继承
上面我们熟悉了自身属性和原有属性,那么如果想让chef1 既能拥有 Chef 的属性,又能使用 Chef的方法,该如何实现呢? 这就要聊到继承了
Chef现在有cook 和 introduce 方法,如果想定义一个 AdvancedChef (高级厨师) 呢。AdvancedChef肯定也有cook 和 introduce,当然还有更高级的技能,比如 train(培训初级厨工)。
// === 子类:高级厨师 AdvancedChef === // 高级厨师比普通厨师多一个"带徒年限"属性,以及"培训"方法 function AdvancedChef(name, yearsOfTeaching) { // Step 1: 借用父类构造函数(继承属性) // 让 Chef 在 AdvancedChef 的实例上执行一遍,把 name 写到自身上 Chef.call(this, name); this.yearsOfTeaching = yearsOfTeaching; // 子类独有的属性 } // AdvancedChef.prototype.constructor === AdvancedChef // true const chef3 = new AdvancedChef('wangwu',3) // chef3.name // wnagwu // chef3.yearsOfTeaching // 3 // chef3.cook('aaa') // chef3.cook is not a function // AdvancedChef.prototype // { constructor: f } // Chef.prototype // {cook: f, introduce: f, constructor: f}从后面几行打印信息看,这个AdvancedChef 目前还没有cook 和introduce, 只有name
并且AdvancedChef.prototype 还是默认空对象,没有连上 Chef.prototype, 此时 AdvancedChef 的实例chef3 能访问 name 属性,但访问不到 cook/introduce 方法。
// 用 Object.create 创建一个新对象,它的 __proto__ 指向 Chef.prototype // 然后把这个新对象赋值给 AdvancedChef.prototype AdvancedChef.prototype = Object.create(Chef.prototype); // 输入 AdvancedChef.prototype // Chef{} 点击开可以看到cook: f(food) , introduce: f() // AdvancedChef.prototype.__proto__ // {cook: f, introduce: f, constructor: f} // 修复 constructor // 因为 上面 替换了 AdvancedChef.prototype,新对象的 constructor 默认指向 Chef(来自原型链) // 必须手动改回来,否则 advancedChef.constructor 返回的是 Chef 而不是 AdvancedChef AdvancedChef.prototype.constructor = AdvancedChef;可见AdvancedChef.prototype.__proto__ === Chef.prototype, AdvancedChef 的实例沿着 __proto__ 就能找到 cook/introduce 了,原型链已连接!AdvancedChef.prototype.__proto__ → Chef.prototype。
继承了Chef的方法,那么现在开始添加高级厨师独有的方法train
// 添加子类自己的方法 AdvancedChef.prototype.train = function(apprenticeName) { return `${this.name}(教了${this.yearsOfTeaching}年)正在培训 ${apprenticeName}`; };添加完成了,我们重新创建一个实例进行验证吧,因为chef3早就创建了,所以它没有cook等方法
const masterChef = new AdvancedChef("张大师", 15)') // 自身属性 - masterChef.name 张大师 //自身属性 - masterChef.yearsOfTeaching 15 //继承方法 - masterChef.cook("佛跳墙") 张大师正在烹饪佛跳墙 //继承方法 - masterChef.introduce() 我是 张大师 //子类方法 - masterChef.train("小王") 张大师(教了15年)正在培训 小王 // 验证属性来源: //masterChef.hasOwnProperty("name") true(自身属性,来自 Chef.call) //masterChef.hasOwnProperty("yearsOfTeaching") true(自身属性,AdvancedChef 自己的) //masterChef.hasOwnProperty("cook") false(原型属性,在 Chef.prototype 上) //masterChef.hasOwnProperty("train") false(原型属性,在 AdvancedChef.prototype 上) // instanceof 验证原型链关系: //masterChef instanceof AdvancedChef true //masterChef instanceof Chef true //masterChef instanceof Object true💡 继承的本质 = 属性借用 + 原型链连接
💡 Chef.call(this, name) 让子类实例拥有了父类的属性
💡 Object.create(Chef.prototype) 让子类原型能访问到父类的方法
💡 高级厨师既有普通厨师的能力,又有自己的绝招
到现在为止,其实我们完成了一个简单的继承,你觉得写起来麻烦吗,回答是肯定的。这就是ES5的写法。那么ES6+出现后,是如何简化的呢,不了解的话可以先看看文章ES6革新:让JavaScript告别“玩具语言”
// ES6 写法 class Chef { constructor(name) { this.name = name; } cook(food) { return \`\${this.name} 正在烹饪 \${food}\`; } } // 继承 class AdvancedChef extends Chef { constructor(name, yearsOfTeaching) { super(name); this.yearsOfTeaching = yearsOfTeaching; } train(apprenticeName) { return \`\${this.name}正在培训 \${apprenticeName}\`; } } // 这等价于 ES5 写法 👇 function Chef(name) { this.name = name; } Chef.prototype.cook = function(food) { return \`\${this.name} 正在烹饪 \${food}\`; } } // 继承 function AdvancedChef(name, yearsOfTeaching) { Chef.call(this, name); // ← super(name) this.yearsOfTeaching = yearsOfTeaching; } AdvancedChef.prototype = Object.create(Chef.prototype); // ← extends 干的事 AdvancedChef.prototype.constructor = AdvancedChef; // ← 自动修复AdvancedChef.prototype.train = function(apprenticeName) { ... }; // ← 类方法`