Why 继承?
- 实现代码的复用(key point)
- 引入一套类型系统的规范
What 继承
继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。
类 & 实例
举个例子,假如动物是一个类,那么人是动物的一个子类。在这句话中,动物是父类,人就是子类。人是一个类,但是具体到单个人,单个个体,我是人类的一个实例,你是人类的一个实例。我们相似却又不同,我们都是一类,但是我们都不一样。
// 类 class
Class Person({ some properties }){ ... };
// 实例 instance
var Me = new Person({ name: "", birth : "" ..... });
值得一提的是,在 JavaScript 中,原本是没有类的概念的,只有 prototype 的概念,所以本文所提及的 类和实例 都是需要人为给他添加的语义理解。看起来好像大家都一样,实际上是不同的。 为了方便区分类和实例,会约定俗成地使用大写首字母作为 类名,方便开发过程中区分开来。 同时在本文中,为了更好区分原型继承和伪类继承,我们只使用核心 code 作为继承的方法,而不采用 new
操作符。 new
操作符只作为新建实例中使用。
前置知识
New 操作符
// Function.method(name, func) => Function.prototype.name = func
Function.method('new', function(){
// 1. 创建一个新对象,继承自构造器函数的原型对象,隐式创建一个空对象
// 2. 将构造函数的作用域赋值,新对象被执行[[Prototype]]连接。
var that = Object.create(this.prototype)
// 3. 调用执行构造器函数,并绑定 this 到这个新对象上面,this 指向新对象
var other = this.apply(that, arguments)
// 4. 如果构造函数的返回值不是一个对象,则返回该新对象 return that
return (typeof other === 'object' && other) || that
})
New 操作符的小应用 demo 面试题
function fn() {
this.user = 'hello world'
return 1 // 由于 1 不是对象,所以返回的是构造函数里面的对象
}
var a = new fn()
console.log(a.user) // 'hello world'
// ---------
function fn() {
this.user = 'hello world'
return {} // 直接放回了这个空对象
}
var a = new fn()
console.log(a.user) // undefined
How 继承
原型式 Prototype
摈弃传统却又在 JS 中有些许怪异的“类”的想法,基于原型的继承比起基于类的继承更加易于理解:一个对象可以继承旧的对象
- 通过字面量去构造一个对象
var Animal = {
name: 'animal',
arr: [1],
get_name: function () {
console.log('this is inner func ' + this.name)
},
getName: function () {
console.log(this.name + ' hello')
return this.name
},
}
- 通过
Object.create(Animal)
构造子类
var Cat = Object.create(Animal)
console.log(Object.getPrototypeOf(Cat) === Animal) // true 说明以及绑定了原型链
Cat.says = function () {
console.log('cat ' + this.name)
}
// var mycat = Object.create(Cat) // 实例 也可以使用 new 构造实例
// var mycat2 = Object.create(Cat) // 实例 也可以使用 new 构造实例
var mycat1 = new Cat('cici')
var mycat2 = new Cat('mimi')
mycat.name = 'mimi'
mycat2.getName()
mycat.says()
mycat2.arr.push('2')
console.log(mycat.arr, mycat2.arr) // [1,2] [1,2] 说明了原型继承无法隔离父对象的属性
Advanced
- 利用
object.assign
实现混合继承。原理是object.assign( target, source1, source2 )
将source1
上可枚举的属性值赋值给target
。但是注意,当有同名时,最后一个出现的属性将会覆盖。通过这个方式还可以实现混合继承,但是没必要。
小结
原型链继承的方法可以专注于对象本身,利用的是一个新对象可以继承旧对象的属性。通过在对象字面量构造属性,在原型链上绑定方法,实现一个通用的模板。子对象通过 object.create
的方式继承父对象的属性及方法,同时可以在子对象中新增父对象没有属性或者方法实现差异化继承。但是,在对象上,由于大家共享了父对象的原型链,所以实例与实例之间会相互干扰。
但是,当实例 1 修改了原型链上的属性是,修改的内容也会影响到实例 2 ,实例之间没有隔离属性,相互影响。这是因为:1 、当子对象拥有和父对象同名的属性或者方法,会覆盖父对象的属性或者方法,如果不存在时,就会沿着原型链逐层往上进行寻找直至找到该属性或者方法。2、包含着引用类型值的原型,修改时也会影响其他的引用该地址的值对象。
此外,可以看到,子类无法向父类传递参数,或者说,无法在不影响其他实例的情况向父类传递参数。这也是在实际情况中很少直接使用原型链方式的继承的原因。
伪类继承(构造器)
构造函数
- 如果函数调用前面加上了 new 关键字,后面的函数的调用模式就会变成了构造器的调用模式
- 这个模式调用将会隐式创建一个连接到该函数的
prototype
的新对象,同时this
也会被绑定到这个新对象上面**(使用call
或者apply
是伪类继承的核心)** - 如果这个函数 return 的内容不是一个对象,那么就会隐式返回一个 this 的值(该新对象)
function Animal(type) {
this.type = type || 'animal'
this.animal = '123'
this.arr = [1]
this.get_name = function ()
console.log('this is inner func ' + this.name)
}
this.get_type = function () {
console.log(this.type)
}
}
Animal.prototype.getName_proto = function () {
console.log(this.name + ' hello')
return this.name
}
function Cat(name) {
// 核心是 call、apply 绑定 this
Animal.call(this,'cat') // 为防止覆盖,应该提于子类函数体顶部
this.name = name || 'cat'
// 会覆盖
this.get_name = function () {
console.log("this is cat's func " + this.name)
}
}
var mycat1 = new Cat('cici')
var mycat2 = new Cat('mimi')
mycat1.get_name() // this is cat's func cici
mycat1.get_type()
// mycat1.getName_proto() // animal prototype func 无法访问父类原型链上的方法
mycat2.arr.push('2')
console.log(mycat1.arr, mycat2.arr) // [1] , [1, '2']
-
little question: Cat 上面的原型方法,mycat 可以使用么?
关键在
new
操作符上,前文提到,new
操作符会链接原型链+执行构造函数,所以 mycat 是可以访问 Cat 类上的原型方法。
function Cat(name) {
Animal.call(this, 'cat')
// your code here
...
}
Cat.prototype.getName_proto = function () {
console.log('prototype')
}
var mycat1 = new Cat('cici')
mycat1.getName_proto() // 'prototype' 可以访问
- 构造器需要接收一大串参数的时候,将函数参数列表改为对象会更加友好
// var myObj = maker(a,b,c)
var myObj = maker({ first:a, second:b, third:c })
小结
实际上伪类继承(构造器)是通过以 call
或者 apply
为核心的代码片段的复用,再通过 new
方法构造实例。通过伪类继承的方法,可以实现 1 属性之间的隔离,2. 父类可以提供一个接收参数,供给未来的子类传入,实现子类构造函数中向父类构造函数传递参数,通过 ParentClass.call(childClass, args.. )
进行参数的传递。为了防止父类覆盖子类方法,调用父类构造函数应该提前至子类函数体顶部。
但是由于父类子类之间,只执行了构造函数,但是没有连接原型链,所以子类没有办法使用父类原型链上的方法。
函数化 (寄生式继承)
迄今为止的继承模式都无法有私有的属性值或者方法,对象的所有属性都是公开可见的,所以我们可以通过函数化继承的方式实现私有化。函数内部可以定义一些私有的函数,只要不绑定到向外返回的对象上时,外部将无法访问到内部的属性或者方法,从而实现私有化。
实际例子:
var Animal = function (spec) {
var that = {} // 实际抛出的对象
var _name = 'private'
var _age = 0
_private_func = function () {
console.log('this is private function')
}
// 只有绑定到 that 上面的才会公开到外面的实例进行调用
that.get_name = function () {
console.log(spec.name)
return spec.name
}
that.says = function () {
console.log(spec.saying || "can't say")
return spec.saying || "can't say"
}
that.getPrivateName = function () {
return _name
}
that.addAge = function (year = 1) {
_age += year
return _age
}
that.getAge = function () {
return _age
}
return that
}
Animal._privateFunc = function () {
console.log('private call')
}
var cat = Animal({ name: 'cat', saying: 'meow' })
// cat.says()
var cat = function (spec) {
spec.saying = 'meow'
var that = Animal(spec)
that.say_my_name = function () {
console.log(that.says() + spec.name + that.says())
return that.says() + spec.name + that.says()
}
_func = function () {
return 'private'
}
return that
}
// console.log(cat.says())
var mycat = cat({ name: 'cai1' })
var mycat2 = cat({ name: 'cai2' })
mycat.addAge(2)
mycat2.addAge(5)
console.log(cat2.getAge())
小结
传入一个对象(可以理解为寄生对象)在函数内部,以某种方式来增强这个对象,然后返回这个对象,从而在外部可以访问该对象。这个对象仅绑定上需要外显的属性或者方法,而私有的方法仅存放在函数体内,从而实现了属性或者方法的私有化。外部引用的对象只能访问到绑定在寄生对象的属性或者方法。任何能够返回新对象的函数都适用于这个模式。
部件化*
把所有复用的函数进行抽取,重心放在了函数本身,而非对象。当对象需要该函数时,将对象传入绑定该函数,从而实现函数代码的复用。
- 定义一个函数,接收 对象 作为参数。 传入的参数绑定该函数后,重新抛出,相当于给对象绑定了函数。
var addFunc = function (obj) {
obj.name = !!obj.name ? obj.name : 'test_name'
obj.Func = function () {
console.log('say my name:' + obj.name)
return 'say my name:' + obj.name
}
return obj
}
var getName = function (obj) {
obj.name = !!obj.name ? obj.name : 'test_name'
obj.getName = function () {
console.log('get my name:' + obj.name)
return obj.name
}
return obj
}
var cat = { name: 'minya' }
cat = addFunc(cat)
cat = getName(cat) // 通过函数化去包装,给这个对象绑定上新的函数方法。实现代码的复用。
console.log(cat)
cat.Func() // 可与包装函数同名,因为指代意义不一样
cat.getName()
总结
本文仅针对 Javascript 精粹这本书的继承部分进行整理。 只是通过最核心的方式实现了最简单的继承模式,在红宝书中还有很多不同类型或者不同类型组合的内容。在实际使用中,上述继承方法不会单独出现。 ref2 提供了与红宝书较为接近的继承模式。 下次有机会再来整理。
Reference:
-
廖雪峰 JAVA 继承
-
? 一文看懂 JS 继承
-
ES6 入门教程
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!