小引
本瓜一向认为:学习不是一蹴而就的事情。一定是要求学习者对知识点进行反复咀嚼拿捏、不断打破重塑,长此以往,才以期达到融会贯通、为我所用的程度。所谓:温故知新,不亦乐乎?
对于 JS 技能拥有者这来说,原型这个概念一定是值得刻在心里去反复玩味的。此篇且暂让本瓜带你再看 JS 原型二三,也许会有新收获,何乐不为
原型的本意
原型概念
JavaScript 常被描述为一种基于原型的语言 (prototype-based language) —— 每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。(MDN)
几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例,这便是“万物皆对象”的一种解释。
原型是我们自然思维的产物。常言道:“照葫芦画瓢”、“照猫画虎”,这里的“葫芦”就是瓢的原型,“猫”就是“虎”的原型。(ps:上周末看了电影《多力特的奇幻冒险》,老虎也喜欢去抓动的光点,太搞笑了,原型继承实锤了!)
原型和类
既然万物皆对象?那你肯定产生过这样的疑问:JavaScript 是面向对象语言(OOP)吗?它为什么没有像 Java 中类的概念?
JavaScript 作者 Brendan Eich(布兰登·艾奇)曾说过:“JavaScript 是 C 语言和 Self 语言一夜情的产物。”(os:的确一夜情,谁能想到,有着百万学习者的语言地创造只花了 10 天?)
他的设计思路是这样的:
- 借鉴 C 语言的基本语法;
- 借鉴 Java 语言的数据类型和内存管理;
- 借鉴 Scheme 语言,将函数提升到"第一等公民"(first class)的地位;
- 借鉴 Self 语言,使用基于原型(prototype)的继承机制。
所以,JavaScript 非严格意义上的面向对象(它没有封装成类),基于原型的继承机制是 JS 深入骨髓、嵌入灵魂的特性。
- 注:ECMAScript 2015(ES6) 中引入的 JavaScript 类实质上是 JavaScript 现有的基于原型的继承的语法糖。类语法不会为JavaScript 引入新的面向对象的继承模型。
TypeScript 大法好
小广告:本瓜最近在浅入深出 TypeScript,已撰文部分,不如点个关注呗,后续更新。
TypeScript 是 JavaScript 的超集,支持面向对象的所有特性,比如 类、接口等。
此处列一小例:
// TypeScript
代码语言:javascript复制class Car {
// 字段
engine:string;
// 构造函数
constructor(engine:string) {
this.engine = engine
}
// 方法
disp():void {
console.log("发动机为 : " this.engine)
}
}
// 编译成 JavaScript(亲测)
代码语言:javascript复制var Car = /** @class */ (function () {
// 构造函数
function Car(engine) {
this.engine = engine;
}
// 方法
Car.prototype.disp = function () {
console.log("发动机为 : " this.engine);
};
return Car;
}());
可以看到 TypeScript 类的定义像极了 Java,编译成 JavaScript 后依然是转成了在原型链上进行操作。
此处本瓜抛一个小问题:基于类一定比基于原型要好吗?它们各自的优势是什么?欢迎讨论~
call、apply、bind
在讲基于原型的继承方式之前,要先知道这三位。我想你一定不会陌生,它们可是手中利器。
call、apply
call 和 apply 是为了动态改变 this 而出现的,当一个 object 没有某个方法,但是其他函数的有,我们可以借助 call 或apply 用其它对象的方法来操作。
代码语言:javascript复制function Free() {
this.free="free"
}
function Food(name1,name2) {// call 接收的是单独的参数
Free.call(this, name1,name2);
}
console.log(new Food('banana','apple').free);
function Foods(arr){ // apply 接收的是数组
Free.apply(this, arr);
}
console.log(new Food(['banana','apple']).free);
bind
bind 用法和 call 类似,只不过调用 bind 后方法不能立即执行(因为其返回的是函数),需要再次调用,其实就是柯里化的一个语法糖。
代码语言:javascript复制var food={name:"apple",func:function(val){console.log(this.name val)}}
var banana=food.func.bind({name:'banana'},'good taste')
banana()
这里为函数柯里化挖一个坑,待填。
继承
重点来啦!如果你想掌握好 JS 原型基础,以下的五种继承方式,请一定烂熟于心。自己动动小手,F12 在控制台上调一下吧?
原型链继承
- 原型链
原型链?不要那些繁琐的官方解释。
本瓜只记住一个:当查找某一个对象的属性/方法的时候,如果自己没有这个属性/方法,则去找创建这个对象的构造函数里面去找,还找不到,就继续向上找,直到查到 Object.prototype.proto
,这样一个链式查找的过程,就是原型链。
let obj = new Object()
obj.__proto__ === Object.prototype // true
Object.prototype.__proto__=null // 万物皆空有木有
- 原型链继承
优点:能够继承父类的原型方法。(示例一)
缺点:原型上的所有属性都是共享的,所以任何一个子类实例修改了原型中的属性(引用类型),其他实例获取到的属性值也会引起变化。(示例二)
示例一:
// 你父母有房,你就有房
代码语言:javascript复制function Parent(){
this.house='我有房'
}
function Son(){
}
Son.prototype = new Parent()
Son.prototype.constructor=Son // 为了更严谨,更改默认指回
var son1=new Son()
console.log(son1.house) // 我有房
Son.prototype.constructor
舒服啦,《我的区长父亲》系列。
示例二:
// 如果有两个儿子,父母只一套房,他们各自想刷不同颜色的墙
代码语言:javascript复制function Parent(){
this.houseColor=[]
}
function Son(){
}
Son.prototype = new Parent()
Son.prototype.constructor=Son
var son1=new Son()
son1.houseColor.push('刷白墙')
var son2=new Son()
son2.houseColor.push('刷红墙')
console.log(son1.houseColor) // ["刷白墙", "刷红墙"]
这下估计就要干仗了!老大刚刷的白墙,被老二又给刷红了。。。
构造继承
优点:解决父类属性是引用类型被所有实例共享的问题和给子类传参的问题。(示例三)
缺点:不能继承父类超类型的原型方法。(示例四)
示例三:
// 解决两个儿子刷墙问题
代码语言:javascript复制function Parent(){
this.houseColor=[]
}
function Son(){
Parent.call(this) // 更改 this 指向
}
var son1 = new Son()
son1.houseColor.push('刷白墙')
var son2 = new Son()
son2.houseColor.push('刷红墙')
console.log(son1.houseColor) // ["刷白墙"]
这下大儿子不会生气了,他刷的白墙还是白墙。
示例四:
// 一波刚平,一波又起。这下没有继承原型对象。
代码语言:javascript复制function Parent(){
}
function Son(){
Parent.call(this) // 更改 this 指向
}
Parent.prototype.getCar = function(){
return '我有车'
}
var son1 = new Son()
son1.getCar() // getCar is not a function
组合继承
组合继承 == 原型链继承 构造继承
优点:二者优点
缺点:父类构造函数执行两次的问题。(示例五)
示例五:
// 组合继承:我全都要。(os:全都要,开销就大。。。)
代码语言:javascript复制function Parent(){
this.houseColor=[]
}
function Son(){
Parent.call(this) // 更改 this 指向
}
Parent.prototype.getCar = function(){
return '我有车'
}
Son.prototype = new Parent()
Son.prototype.constructor = Son
var son1 = new Son()
son1.houseColor.push('刷白墙')
var son2 = new Son()
son2.houseColor.push('刷红墙')
console.log(son1.houseColor,son1.getCar()) // ["刷白墙"] "我有车"
console.log(son2.houseColor,son2.getCar()) // ["刷红墙"] "我有车"
这样又刷了墙,又获得了车。一家人其乐融融,就是父母的压力有点大。
寄生组合继承
为了解决组合继承的缺点,于是有了寄生组合继承。
实质是:通过Object.create(obj)创建一个原型是 obj 的空对象赋值给子类的原型。
示例六:
代码语言:javascript复制function Parent(){
this.houseColor=[]
}
function Son(){
Parent.call(this) // 更改 this 指向
}
Parent.prototype.getCar = function(){
return '我有车'
}
Son.prototype = Object.create(Parent.prototype)
Son.prototype.constructor = Son
var son1 = new Son()
son1.houseColor.push('刷白墙')
var son2 = new Son()
son2.houseColor.push('刷红墙')
console.log(son1.houseColor,son1.getCar()) // ["刷白墙"] "我有车"
console.log(son2.houseColor,son2.getCar()) // ["刷红墙"] "我有车"
ES6 继承
通过 class,extends 关键字实现继承。需要清楚的是:ES6 中的类是一个语法糖,本质上还是由 ES5 的语法实现的。
示例七:
代码语言:javascript复制class Parent{
constructor(){
this.houseColor = []
}
getCar(){
return '我有旧车'
}
}
class Son extends Parent{
constructor(color){
super()
this.houseColor = color
}
getCar(){
return "我有新车"
}
}
const son1 = new Son("刷白墙")
const son2 = new Son("刷红墙")
console.log(son1.houseColor,son1.getCar()) // ["刷白墙"] "我有车"
console.log(son2.houseColor,son2.getCar()) // ["刷红墙"] "我有车"
发布订阅模式
这里为什么要把发布订阅模式点出来呢?
因为它也涉及多种情况。
- 多人订阅一个发布。
- 多人订阅多个发布。
- 一人订阅一个发布(示例八)。
- 一人订阅多个发布。
每一种都值得动手去写一些,玩一玩。
示例八:
代码语言:javascript复制/*paper*/
var paper={
listen:'',
addlisten:function(fn){//增加订阅者
this.listenList=fn;
},
trigger:function(){//发布消息
this.listenList.apply(this,arguments);
}
}
/*订阅*/
paper.addlisten(function(val){
console.log("小王订阅消息:" val);
});
/*发布*/
paper.trigger("新闻周刊到了");
这里为24 种设计模式挖一个坑,待填。
小结
本篇是本瓜浅入深出 TS 过程中的衍生篇,这些老生常谈的东西偶尔拿出来再看看,其实感觉真的还不错!(还有比如this、作用域这些没细说)最重要的是自己能在控制台写一写。旧的知识点和新的知识点产生碰撞的时候,便是收获的时候。
进一寸有进一寸的欢喜,如是而已。
我是掘金安东尼,与你同行!
参考
- 对象原型
- 继承与原型链
- JavaScript 到底是面向对象还是基于对象?-winter
- Javascript 诞生记
- JavaScript 实现继承的方式