面向对象编程就是将需求抽象成一个对象,然后针对这个对象分析其特征(属性)与动作(方法),将需要的功能放在一个对象里,就是封装。
在JavaScript中创建一个类很容易,首先声明一个函数并将其保存在一个变量(首字母大写)里,然后在这个类里面通过对this变量添加属性 & 方法,从而使他们同样添加到类上。如下创建一个书类:
var Book = function (id, bookname, price) {
this.id = id;
this.bookname = bookname;
this.price = price;
}
同样可以在类的原型prototype上添加属性和方法:
Book.prototype.display = function () {
// 展示书的名字
}
或者:
Book.prototype = {
display: function () {
// 展示书的名字
}
}
这样一来,就将所需要的方法和属性都封装在我们抽象的Book类之中了,当使用功能方法时,不能直接使用这个Book类。
此时需要用 new 关键字来实例化(创建)新的对象。使用实例化对象的属性或者方法时,可以通过点语法访问,例如:
var book = new Book(10, '书名', 50)
console.log(book.bookname) // 书名
JavaScript 是一种基于原型 prototype 的语言,每创建一个对象时,它都有一个原型 prototype 用于指向其继承的属性、方法。
这样通过prototype继承的方法并不是对象自身的,所以在使用这些方法时,需要通过prototype一级一级查找来得到。
所以
1、this添加的属性、方法是在当前对象上添加的,是属于该对象自身拥有的,所以我们每次通过类创建一个新对象时,this指向的属性和方法都会得到相应的创建。
2、通过
prototype继承的属性或者方法是每个对象通过prototype访问到,所以我们每次通过类创建一个新对象时这些属性和方法不会再次创建。
如上图所示。
图中的
constructor是一个属性,当创建一个函数或者对象时都会为其创建一个原型对象prototype,在prototype对象中又会像函数中创建this一样创建一个constructor
属性,那么constructor属性指向的就是拥有整个原型对象的函数或对象
由于JavaScript
的函数级作用域,声明在函数内部的变量以及方法在外界是访问不到的,通过此特性即可创建类的私有变量以及私有方法。
而在函数内部通过this创建的属性和方法,在类创建对象时,每个对象自身都拥有一份并且可以在外部访问到。
因此通过this创建的属性可以看作是对象共有属性和对象共有方法,通过this
创建的方法,不但可以访问这些对象的共有属性与共有方法,而且还能访问到类(创建时)或对象自身的私有属性和私有方法。
由于这些方法权利比较大,所以我们又将其看作特权方法。
在对象创建时通过使用这些特权方法我们可以初始化实例对象的一些属性,因此这些在创建对象时调用的特权方法还可以看作是类的构造器。
var book = new function(id, name, price) {
// 私有属性
var num = 1;
function checkedId() {}
// 特权方法
this.getName = function () {}
this.getPrice = function () {}
this.setName = function () {}
this.setPrice = function () {}
// 对象共有属性
this.id = id
this.copy = function () {}
// 构造器
this.setName(name)
this.setPrice(price)
}
这里需要注意的是,通过new关键字创建新对象时,由于类外面通过点语法添加的属性和方法没有执行到,所以新创建的对象中无法获取到他们,但是可以通过类来使用。
因此通过点语法定义的属性以及方法被称为类的 静态共有属性 和 静态共有方法 。
而类通过 prototype 创建的属性或者方法在类的实例对象中可以通过 this 访问到,所以我们将 prototype 对象中的属性和方法称为共有属性和共有方法。如下:
// 类静态公有属性和方法(对象不能访问)
Book.isChinese = true
Book.resetTime = function () {
console.log('new Tiem')
}
Book.prototype = {
// 公有属性和方法
isJSBook: false,
display: function () {}
}
通过 new 关键字创建的对象实质是对新对象 this 的不断赋值,并将prototype 指向类的 prototype 所指的对象,而类的构造函数外面通过点语法定义的属性方法是不会添加到新创建的对象上去的。
因此要在新对象中使用 isChinese 就得通过 Book 类使用而非 this,如 Book.isChinese。
闭包 是有权访问另一个函数作用域中变量的函数,即在一个函数内创建另外一个函数。
我们将闭包作为创建对象的构造函数,使它既是闭包又是可实例对象的函数,它可以访问到类函数作用域中的变量。
将类的静态变量通过闭包来实现是很常见的现象:
// 利用闭包实现
var Book = (function () {
// 静态私有变量和方法
var bookNum = 0
function checkBook (name) {}
// 返回构造函数
return function (newId, newName, newPrice) {
// 私有变量和方法
var name, price
function checkID (id) {}
// 特权方法
this.getName = function () {}
this.getPrice = function () {}
this.setName = function () {}
this.setPrice = function () {}
// 公有属性
this.id = newId
this.copy = function () {}
bookNum++
if (bookName > 100) throw new Error('我们仅出版100本书')
// 构造器
this.setName(name)
this.setPrice(price)
}
})()
上面的代码是在闭包外部添加原型属性和方法,看上去像是脱离了闭包这个类,所以有时候在闭包内部实现一个完整的类然后将其返回,如下面的例子。
// 利用闭包实现
var Book = (function () {
// 静态私有变量和方法
var bookNum = 0
function checkBook (name) {}
// 返回构造函数
return function (newId, newName, newPrice) {
// 私有变量和方法
var name, price
function checkID (id) {}
// 特权方法
this.getName = function () {}
this.getPrice = function () {}
this.setName = function () {}
this.setPrice = function () {}
// 公有属性
this.id = newId
this.copy = function () {}
bookNum++
if (bookName > 100) throw new Error('我们仅出版100本书')
// 构造器
this.setName(name)
this.setPrice(price)
}
})()
对于初学者来说,在创建对象上由于不适应这种写法,所以经常容易忘记使用new而犯错误。针对这种情况,js在创建对象时有一种安全模式就可以完全解决这类问题。
// 图书安全类
var Book = function (title, time, type) {
// 判断执行过程中 this 是否是当前这个对象(如果是说明是用new创建的)
if (this instanceof Book) {
this.title = title
this.time = time
this.type = type
// 否则重新创建这个对象
} else {
return new Book(title, time, type)
}
}
// 测试
var book = Book('面向对象', '2020', 'js')
console.log(book) // Book
console.log(book.title) // 面向对象
console.log(book.time) // 2020
console.log(book.type) // js
console.log(window.title) // undefined
JavaScript 没有现有的 继承 这一机制。但也正因为少了这些显性的限制才具有了一定的灵活性,从而可以根据不同的需求来实现多样式的继承。
比如常见的类式继承:
// 类式继承
// 声明父类
function SuperClass () {
this.superValue = true
}
// 为父类添加共有方法
SuperClass.protptype.getSuperValue = function () {
return this.superValue
}
// 声明子类
function SubClass () {
this.subValue = false
}
// 继承父类
SubClass.prototype = new SuperClass ()
SubClass.prototype.getSubValue = function () {
return thie.subValue
}
类式继承需要将 父类的实例 给 子类的原型。
类的原型对象 的作用就是为 类的原型 添加共有方法,但类不能直接访问这些属性和方法,必须通过原型 prototype 来访问。
而我们实例化一个父类的时候,新创建的对象复制了 父类的构造函数内的属性与方法 ,并且将原型_proto_指向了父类的原型对象,这样它也就拥有了父类原型的属性与方法,并且这个新创建的对象可直接访问到这些属性与方法。
新创建的对象不仅仅可以访问父类原型上的属性和方法,同样也可以访问从父类构造函数中复制的属性和方法。将这个对象赋值给子类的原型,那么这个子类的原型同样可以访问父类原型上的属性和方法,以及从父类构造函数中复制的属性和方法。
这就是类的继承原理。下面使用子类:
var instance = new SubClass()
console.log(instance.getSuperValue()) // true
console.log(instance.getSubValue()) // false
这种类式继承有两个缺点:
1、由于子类通过其原型prototype对父类实例化,继承了父类。所以说父类中的共有属性要是有引用类型,就会在子类中被所有实例共用。
因此一个子类的实例更改子类原型,从父类构造函数中继承来的共有属性就会直接影响到其他子类:
function SuperClass () {
this.books = ['js', 'html', 'css']
}
function SubClass () {}
SubClass.protptype = new SuperClass()
var instance1 = new SubClass()
var instance2 = new SubClass()
console.log(instance2.books) // ["JavaScript", "html", "css"]
instance1.books.push('新版教程')
console.log(instance2.books) // ["JavaScript", "html", "css", "新版教程"]
// instance1 一个无意的修改就会无情地伤害了 instance2 的 book 属性,这在编程中很容易埋藏陷阱
2、由于子类实现的继承是靠其原型 prototype 对父类实例化实现的,因此在创建父类的时候,是无法向父类传递参数的,因而在实例化父类的时候,也无法对父类构造函数内的属性进行初始化。
而 JavaScript 是灵活的,这两个问题也不是没有办法解决,我们可以使用另一种方式实现。
// 构造函数式继承
// 声明父类
function SuperClass(id) {
// 引用类型共有属性
this.books = ['js', 'html', 'css']
// 引用类型共有属性
this.id = id
}
// 父类声明原型方法
SuperClass.protptype.showBooks = function () {
console.log(this.books)
}
// 声明子类
function SubClass (id) {
// 继承父类
SuperClass.call(this, id)
}
// 创建第一个子类的实例
var instance1 = new SubClass(10)
// 创建第二个子类的实例
var instance2 = new SubClass(11)
instance1.books.push("面向对象")
console.log(instance1.books) // ["js", "html", "css", "面向对象"]
console.log(instance1.id) // 10
console.log(instance2.books) // ["js", "html", "css"]
console.log(instance2.id) // 11
instance1.showBooks() // TypeError
SuperClass.call(this, id) 这条语句是构造函数式继承的精华,由于 call 这个方法可以更改函数的作用环境,因此子类中,对 superClass 调用这个方法就是将子类中的变量在父类中执行一遍,由于父类中是给 this 绑定属性的,因此子类自然也就继承了父类的共有属性。
但这种方式同样存在缺陷,由于这种类型的继承没有涉及原型 prototype ,所以父类的原型方法自然不会被子类继承,而如果想要被子类继承就必须放在构造函数中,这样创建出来的每个实例都会单独拥有一份而不能共用,这样就违背了代码复用的原则。
综合以上两种模式的优点,就有了 组合式继承 。
类式继承是通过子类的原型 prototype 对父类实例化来实现的,构造函数式继承是通过在子类的构造函数作用环境中执行一次父类的构造函数来实现的,所以只要在继承中同时做到这两点即可:
// 组合式继承
// 声明父类
function SuperClass (name) {
// 值类型公有属性
this.name = name
// 引用类型共有属性
this.books = ["html", "css", "JavaScript"]
}
// 父类原型共有方法
SuperClass.prototype.getName = function () {
console.log(this.name)
}
// 声明子类
function SubClass (name, time) {
// 构造函数式继承父类name属性
SuperClass.call(this, name)
this.time = time
}
// 类式继承 子类原型继承父类
SubClass.prototype = new SuperClass()
// 子类原型方法
SubClass.prototype.getTime = function () {
console.log(this.time)
}
如此一来,子类的实例中更改父类继承下来的引用类型属性如 books,根本不会影响到其他实例,并且子类实例化过程中又能将参数传递到父类的构造函数中,如 name:
var instance1 = new SubClass("js book", 2014)
instance1.books.push("红色的书")
console.log(instance1.books) // ["html", "css", "JavaScript", "红色的书"]
instance1.getName() // js book
instance1.getTime() // 2014
var instance2 = new SubClass("css book", 2013)
console.log(instance2.books) // ["html", "css", "JavaScript"]
instance1.getName() // css book
instance1.getTime() // 2014
上面的代码中,
1、子类实例更改从父类继承下来的引用类型 books ,不会影响到其他实例。
2、子类实例化的过程中,又能将参数传递到父类的构造函数中,如 name。
但在使用构造函数继承时执行了一遍父类的构造函数,而在实现子类原型的类式继承时又调用了一遍父类构造函数。因此父类构造函数调用了两遍。所以也不是最理想的继承方式。
借助原型 prototype 可以根据已有的对象创建一个新的对象,同时不必依赖新创建的自定义对象类型。
// 原型式继承
function inheritObject (o) {
// 声明一个过度函数对象
function F () {}
// 过渡对象的原型继承父对象
F.prototype = o
// 返回过渡对象的一个实例,该实例的原型继承了父对象
return new F()
}
这种方式和类式继承有些类似,它是对类式继承的一个封装,其实其中的过渡对象就相当于类式继承中的子类,只不过在原型式中作为一个过渡对象出现的,目的是为了要返回的新的实例化对象:
var book = {
name: "js book",
alikeBook: ["css book", "html book"]
}
var newBook = inheritObject(book)
newBook.name = "ajax book"
newBook.alikeBook.push("xml book")
var otherBook = inheritObject(book)
otherBook.name = "flash book"
otherBook.alikeBook.push("as book")
console.log(newBook.name) // ajax book
console.log(newBook.alikeBook) // ["css book", "html book", "xml book", "as book"]
console.log(otherBook.name) // flash book
console.log(newBook.alikeBook) // ["css book", "html book", "xml book", "as book"]
console.log(book.name) // js book
console.log(newBook.alikeBook) // ["css book", "html book", "xml book", "as book"]
这种继承方式的好处在于:F 过渡类的构造函数中无内容,所以开销比较小,使用起来比较方便。
寄生式继承就是对原型继承的第二次封装,并且在这第二次封装过程中,对继承的对象进行了拓展,这样的新创建对象的继承思想也是寄托于原型继承模式。
// 寄生式继承
// 声明基对象
var book = {
name: "js book",
alikeBook: ["css book", "html book"]
}
function createBook (obj) {
// 通过原型继承方式创建新对象
var o = new inheritObject(obj)
// 拓展新对象
o.getName = function () {
console.log(name)
}
// 返回拓展后的新对象
return o
}
这种思想的作用也是为了寄生组合式继承模式的实现。
这种继承方式可以看作是组合式继承的延续。寄生是指寄生式继承,寄生式继承依托于原型继承,原型继承又与类式继承很像。所以就是寄生式继承 + 构造函数继承
// 寄生式继承 继承原型
// 传递参数 subClass 子类
// 传递参数 superClass 父类
function inheritPrototype (subClass, superClass) {
// 复制一份父类的原型副本保存在变量中
var p = inheritObject(superClass.prototype)
// 修正因为重写子类原型导致子类的 constructor 属性被修改
p.constructor = subClass
// 设置子类的原型
subClass.prototype = p
}
组合式继承中,通过构造函数继承的属性和方法是没有问题的,所以这里我们主要探究通过寄生式继承重新继承父类的原型。我们需要继承的仅仅是父类的原型,不再需要调用父类的构造函数,换句话说,在构造函数继承中我们已经调用了父类的构造函数,因此我们需要的就是父类的原型对象的一个副本,而这个副本我们通过原型继承便可得到,但是这么直接赋值给子类会有问题的,因为对父类原型对象复制得到的复制对象p中的 constructor 指向的不是 subClass 子类对象,因此在寄生式继承中要对复制对象p做一次增强,修复其constructor指向不正确的问题,最后将得到的复制对象p赋值给子类的原型,这样子类的原型就指向了父类的原型并且没有执行父类的构造函数。
// 定义父类
function SuperClass (name) {
this.name = name
this.colors = ["red", "blue", "green"]
}
// 定义父类原型方法
SuperClass.prototype.getName = function () {
console.log(this.name)
}
// 定义子类
function SubClass (name, time) {
// 构造函数式继承
SuperClass.call(this, name)
// 子类新增属性
this.time = time
}
// 寄生式继承父类原型
inheritPrototype(SubClass, SuperClass)
// 子类新增原型方法
SubClass.prototype.getTime = function () {
console.log(this.time)
}
// 创建两个测试方法
var instance1 = new SubClass("js book", 2014)
var instance2 = new SubClass("css book", 2013)
上面的代码首先创建了父类,以及父类的原型方法,然后创建了子类,并在构造函数中实现构造函数式继承,然后又通过寄生式继承了父类原型,最后又对子类添加了一些原型方法。
instance1.colors.push("black")
console.log(instance1.colors) // ["red", "blue", "green", "black"]
console.log(instance2.colors) // ["red", "blue", "green", "black"]
instance2.getName()
instance2.getTime()
}
这种继承方式最大的改变就是对子类原型的处理,被赋予父类原型的一个引用,这是一个对象,因此这里有一点需要注意,就是子类再想添加原型方法必须通过 prototype.对象 ,通过点语法的形式一个一个添加方法了,否则直接赋予对象就会覆盖掉从父类原型继承的对象了。