多态
同一个操作作用于不同的对象,得到不同的结果。
重点分离:做什么 和 谁去做。
鸭子辩型:如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。
// 做什么
function sayName( people ) {
people.sayName()
}
// 谁去做
const xiaoming = {
name: '小明',
sayName() {
console.log( this.name )
}
}
const lihong = {
name: '李红',
sayName() {
console.log( this.name )
}
}
// 同一个操作作用在不同的对象
sayName( xiaoming ) // 小明
sayName( lihong ) // 李红
封装
只让别人知道你想让别人知道的,不让别人知道你不想让别人知道的。
封装的目的是将信息隐藏,数据、类型、实现、变化都能封装。
- 封装数据
Javascript 中要达到封装的目的,只有通过创建作用域来达到隐藏的目的。
const xiaoming = ( function() {
// 封装私有数据,变量命名前缀加_下划线是潜规则,表示是私有的
const _name = '小明',
_age = 22
// 向外界提供公共方法
return {
getName() { return _name },
getAge() { return _age }
}
})()
xiaoming.getName() // '小明'
xiaoming.getAge() // 22
xiaoming._name // undefined
xiaoming._age // undefined
- 封装类型
Javascript是弱类型语言,封装类型没多大意义。(完)
- 封装实现
对象只需要对外提供一个固定的接口,而内部如何实现这个接口,对外界来说应该是透明的,外界不用关心这个对象内部如何去实现这个接口,外界只需要调用这个接口就行了。只要这个接口不变,内部的实现如何去改变都不会影响外界。只要你打一个电话给我,你都会得到我送给你的一颗同品牌同口味同款式的棒棒糖,你不用管我怎么得到这颗棒棒糖,你想吃的时候只需要打一个电话给我就好了嘿嘿嘿。
/**
* 有一个 obj对象,这个对象有一个接口
* 外界只要调用这个接口,就会返回一个数字 666
* 接口的实现对于外界来说是透明的,外界只需调用接口
*/
const obj = ( function() {
let _num = 666
return {
generate666() { return _num }
}
})()
// 外界只需要调用这个对象的generate666接口就行了
obj.generate666() // 666
/**
* 某天这个对象的内部实现改了,但是接口没有改
* 依旧是对外返回一个数字 666
*/
const obj = ( function() {
let _num = 600 + 60 + 6
return {
generate666() { return _num }
}
})()
// 外界依旧只是调用这个对象的generate666接口就行了
obj.generate666() // 666
- 封装变化:
考虑你的设计中哪些地方可能变化,这种方式与关注会导致重新设计的原因相反。它不是考虑什么时候会迫使你的设计改变,而是考虑你怎样才能够在不重新设计的情况下进行改变。这里的关键在于封装发生变化的概念,这是许多设计模式的主题。
----《设计模式》
找到变化的东西,封装起来,和不变的分开。这样就能够容易地把变化的东西替换掉。
继承
Javascript 是基于原型的语言,没有类的概念。但是你可以用类的概念去理解,实际上在 Javascript 中,“类”也是一个对象。
原型模式是一种设计模式,也是一种编程泛型。
在诸如 JAVA 或者 Python 这种以类为中心的面向对象的语言来说,类和对象的关系就像是设计图和成品的关系。要创造成品(对象),就先设计图纸(类),然后按照图纸(类)制造出(new)成品(对象)。
而在 Javascript 中,虽然有 new 这个关键字,但是他做的事情和 JAVA 或者 Python 做的事情不一样。
Javascript 中 new 做的事情是通过找到一个对象的原型进行克隆,获得新对象。
Javascript 中绝大部分数据都是对象,根对象是 Object.prototype
,所有对象都克隆这个根对象。
// 所有对象都克隆 Object.prototype
const o1 = {}
const o2 = new Object()
Object.getPrototypeOf( o1 ) === Object.prototype // true
Object.getPrototypeOf( o2 ) === Object.prototype // true
// 如果在 Object.prototype 上增加一个属性,那么所有对象都会增加这个属性
Object.prototype.six = 666
const o3 = {}
const o4 = new Object()
const o5 = new Date()
const o6 = new Number( 66 )
o3.six // 666
o4.six // 666
o5.six // 666
o6.six // 666
要创建一个新对象,不是通过实例化类,而是找到一个原型对象进行克隆,得到新对象。
而在现代浏览器中,都会向外暴露一个 __proto__
属性(在ECMA规范中的描述是 [[ Prototype ]]
),这个属性指向这个对象的原型对象。原型对象会有一个 constructor
属性来存储它的构造函数名。
这样在访问一个对象的时候,如果本身有属性,就返回本身的属性,如果没有,对象会去它的原型对象上找这个属性,如果原型对象上还没有,又会去原型对象的原型对象找,一层一层找,如果你愿意一层一层一层地拨开原型对象的属性,直到尽头 null
。如果找不到了,你会发现,你会流泪,就返回 undefined
。这不就是原型链的原理吗嘿嘿嘿。
这个克隆的过程是引擎帮忙实现的。但是我们也可以实现一下 new
的过程。
function A() {
this.ownNum = 666
this.ownFn = function() { console.log(123) }
}
A.prototype.protoNum = 888
A.prototype.protoFn = function() { console.log(456) }
A.prototype.constructor = A // 隐含属性,无需设置
const a = new A()
a.constructor === A // true
a.__proto__ === A.prototype // true
/** new 的过程 **/
// 克隆 Object.prototype
const o1 = new Object()
// 让 o1 的 [[ Prototype ]] 指针指向一个原型对象
o1.__proto__ = A.prototype
// A.call( o1 ):给 o1 设置新的属性,o1可能被改变(取决于 A构造函数 中是否使用了 this)
// o2 = A.call( o1 ):A 构造函数可能直接 return 一个新对象,而不是默认 return 构造出来的 o1 对象
// 比如 function A() { ... return {b: 9} },那么在 new A() 之后返回的是 { b: 9 },而不是默认的 o1
const o2 = A.call( o1 )
// 总是返回一个这个新建的对象
// 无论是 o1 还是 { b: 9 },如果有 { b: 9 },那么这个的优先级是最高的
return typeof o2 === 'object' ? o2 : o1
而这上述所有过程涉及到的继承是这样的:(每一个函数都有 prototype
属性)
/*-------------------< 代码的层面 >---------------------*/
// 引擎实现的代码 [native code]
function Object() {}
Object.prototype.xx = xx
// 引擎实现的代码 [native code]
function Function() {}
Function.prototype.yy = yy
// 用户编写的代码
function A() {}
A.prototype.zz = zz
// new一个对象
const a = new A()
/*-------------------< 代码的层面 />---------------------*/
/*-------------------< 背后的逻辑 >---------------------*/
a.__proto__ => A.prototype
A.__proto__ => Function.prototype // typeof A === 'function'
A.prototype.constructor => A
A.prototype.__proto__ => Object.prototype // typeof A.prototype === 'object'
Function.__proto__ => Object.prototype
Function.prototype.constructor => Function
Function.prototype.__proto__ => Object.prototype // typeof Function.prototype === 'function'
Object.__proto__ => Function.prototype // typeof Object === 'function'
Object.prototype.constructor => Object
Object.prototype.__proto__ => null
/*-------------------< 背后的逻辑 />---------------------*/
在访问 a
的时候,如果本身有想要的属性,就会返回本身的属性,此时用 a.hasOwnProperty( 'a自有属性' )
进行属性检测是返回 true
。如果 a
本身的自有属性没有,此时就展现原型链的作用了。a
会通过 a.__proto__
访问到 A.prototype
,在 A.prototype
找到了就返回,如果还没找到,就继续一直往下找,直到终点 null
,还没找到,就返回 undefined
。
可以说,最初的时候,每一个对象都是一样的,都是从 Object.prototype
上克隆而来,都是空白的。但是 Javascript 的世界是缤纷多彩的,大家都一样就没什么意思了。
于是有些对象被人为(引擎)增加了 length
长度的数据属性,有了 push
、shift
等方法属性,再用语法层面的 []
符号表示,就成为了定义中的数组,即Array
function Array() {
this.length = xxx
}
Array.prototype.push = function() {}
Array.prototype.shift = function() {}
var arr = []
// 等价
var arr = new Array()
是不是和 function A() {}
的套路很像?是的。
同样道理,Date
对象也是如此,因为这个对象有了 GMT
时间的数据属性等,又有了 getDay
、getMonth
等方法,就成了定义中的日期对象。
Javascript 的继承的另一种理解,更像是“借用”。类似 Object.prototype.toString.call( ... )
。
继承的本质是改变这个对象的 __proto__
指向。
一个对象有两种属性,自有属性和继承属性。自有属性就是本身自己身上有的东西,访问之后直接就给你返回的,继承属性其实算是借用的属性,是通过访问自身的 __proto__
的指向对象属性,这时候已经是在访问其他对象了,不是自己的属性。
就好比如两个对象其实是不认识,不相关的,但是你把 A对象 的 proto 属性改为 B对象,这两个对象就产生了关系,这时候你也可以称为继承。
这种理解和其他基于 CLASS 类与对象的继承有很大的区别。
此外,继承也是代码重用的手段,Javascript 中的继承不是通过严格意义上的拓展父类来实现的,而是通过原型实现的。
形如 Photoshop,一般都不会直接操作原始图层,都会复制(克隆)一份图层,在复制的图层上进行编辑。