1.语法
1.1 对象的声明语法
1.1.1通过字面量方式新建对象
var obj = {}var obj1 = {name: 'hhupp',call: function() {console.log('CALL')}}console.log('obj', obj)console.log('obj1', obj1)obj1.call()
1.1.2通过new关键词新建对象(不推荐--和通过字面量方式新建对象并无差别--且使用esLint会有eslint(no-new-object)提示报错)
var obj = new Object()console.log('obj',obj)var obj1 = new Object()obj1.name = 'hhupp'obj1.call = function (){console.log('CALL');}console.log(obj1)obj1.call()
1.1.3 通过构造函数新建对象
function mySelfObject (name,method) { // 自己编写的构造函数this.name = namethis.method = method}// 实例化1var obj = new mySelfObject()console.log('obj',obj)// 实例化2var obj1 = new mySelfObject('hhupp',function (){console.log('CALL')})console.log('obj1',obj1)obj1.method()
2.特性
面向对象的三大特性:封装、继承、多态。
2.1 封装
我们举个例子,有两只小狗A和B这两只狗都属于“狗”这个种族,但是它们是相对独立的,A和B拥有自己的名字,品种,年龄,会跑,会叫等等,一只狗就是一个单位,一个整体,然后才会有A和B之间的关系。我们平时所用的方法和类都是一种封装,当我们在项目开发中,遇到一段功能的代码在好多地方重复使用的时候,我们可以 把他单独封装成一个功能的方法,这样在我们需要使用的地方直接调用就可以了。
优点:封装的优势在于定义只可以在类内部进行对属性的操作,外部无法对这些属性指手画脚,要想修改,也只能通过你定义的封装方法;
2.2.1 工厂模式封装
工厂模式的原理:在方法内部创建一个object对象,对象的属性由参数指定,方法直接挂载在对象上,最后返回这个对象。相当于Dog方法是一个造狗工厂,创造了A和B两只狗,
function Dog(name, age, color) {var dog = new Object()dog.name = namedog.age = agedog.color = colorreturn dog
}
var dogA = Dog('A', '1', '白色')
var dogB = Dog('B', '5', '黑色')
console.log('小狗A的属性 == ', dogA)
console.log('小狗B的属性 --> ', dogB)
缺点: 无法知道对象的具体的类型
2.2.2 构造函数模式封装
构造函数模式可以知道小狗A和小狗B这两只狗是Dog类型,原理是new关键字进行了以下的操作。
1. 创建一个全新的对象
2.这个对象会被执行[[prototype]]连接原型
3.方法调用中的this会绑定到新的对象
4.如果构造函数里没有返回其他对象,那么new构造会自动返回这个新的对象
function Dog(name, age, color) {this.name = namethis.age = agethis.color = color
}
var dogA = new Dog('A', '1', '白色')
var dogB = new Dog('B', '5', '黑色')
console.log('小狗A的属性 == ', dogA)
console.log('小狗B的属性 --> ', dogB)
上述的原型链图示如下:
2.2 继承
继承可以解决代码复用的问题,让代码更加接近人类的思维。当多个类存在相同的属性和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,子类只需要通过继承,就可以拥有父类中的属性和方法,不需要重新进行定义。
2.2.1 通过原型链实现继承
让一个构造函数的原型是另一个类型的实例,那么这个构造函数new出来的实例就具有该实例的属性。
当我们试图访问一个对象的属性时,优先从对象本身查找,若未找到,则沿着该对象的原型链逐级往上查找,直到查询到对应的属性或者达到原型链的末尾。
// 父类的构造函数function Parent() {this.flag = falsethis.info = {name: 'hhupp',age: '18'}}Parent.prototype.getInfo = function() {console.log('flag', this.flag)console.log('info', this.info)}// 子类的构造函数function Children() {};// 将子类的原型指向父类的实例Children.prototype = new Parent()// 实例化一个子类1var children1 = new Children()children1.info.name = 'children1'// 实例化一个子类2var children2 = new Children()children2.flag = truechildren2.sex = '女'children2.info.name = 'children2'children1.getInfo() // 获取children1的数据children2.getInfo() // 获取children2的数据
优点:代码方便简洁,便于理解
缺点:对象实例共享所有继承的属性和方法,创建子类型实例时不能传递参数,因为这个对象是一次性创建的(无法进行定制化操作)
2.2.2 借用构造函数继承
在子类型的构造函数中调用父类型的构造函数,通过apply()或者call()将父类的构造函数绑定在子对象上
// 父类的构造函数function Parent(sex) {this.info = {name: 'hhupp',age: '18',sex: sex}}// 子类的构造函数function Children(sex) {Parent.call(this, sex)};var children1 = new Children('男')children1.info.name = 'children1'var children2 = new Children('女')children2.info.name = 'children2'console.log(children1)console.log(children2)
优点:解决了原型链实现继承不能传参的问题和父类原型共享的问题。
缺点:借用构造函数实现继承的缺点是方法都在构造函数中定义,无法实现函数复用。在父类型的原型中定义的方法对子类型来说都是不可见的,导致所有类型只能使用构造函数的模式。
2.2.3 组合继承 (经典继承)
将原型链和借用构造函数的方式组合到一块,使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承,这样既通过再原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性。
// 父类的构造函数function Parent(sex) {console.log('执行父类构造函数' + sex)this.info = {name: 'hhupp',age: '18',sex: sex}}Parent.prototype.getStaffInfo = function() {console.log(this.info.name, this.info.sex)}// 子类的构造函数function Children(sex) {Parent.call(this, sex)};Children.prototype = new Parent()var children1 = new Children('男')children1.info.name = 'children1'var children2 = new Children('女')children2.info.name = 'children2'children1.getStaffInfo()console.log(children1)children2.getStaffInfo()console.log(children2)
优点:解决了原型链继承和借用构造函数继承造成的影响
缺点:无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数的内部。
2.2.4 原型式继承
方法一:借用构造函数在一个函数A内部创建一个临时的构造函数,将传入的对象作为这个构造函数的原型,返回这个临时类型的一个新实例。 函数A本质上是对传入的对象进行了一次浅拷贝。
function createObject(obj) {function Fun() {}Fun.prototype = objreturn new Fun()}let person = {name: 'HHUPP',age: 18,hoby: ['唱', '跳'],showHoby() {console.log('my hoby is:', this.hoby)}}let child1 = createObject(person)child1.name = 'LSY'child1.hoby.push('rap')let child2 = createObject(person)child2.name = 'ZQJ'child2.hoby.push('篮球')console.log(child1)console.log(child2)console.log(person)
方法二:Object.create()
Object.create ()是吧现有对象的属性,挂到新建对象的原型上, 新建对象为空对象ES5通过增加Object.create()方法将原型式继承的概念规范化了,这个方法接收两个参数,作为新对象原型的对象,以及给新对象定义额外属性的对象(可选),在只有一个参数时,Object.create()与上述的方法A效果相同。
let person = {name: 'HHUPP',age: 18,hoby: ['唱', '跳'],showHoby() {console.log('my hoby is:', this.hoby)}}let child1 = Object.create(person)child1.name = 'LSY'child1.hoby.push('rap')let child2 = Object.create(person)child2.name = 'ZQJ'child2.hoby.push('篮球')console.log(child1)console.log(child2)console.log(person)
2.2.5 ES6 Class实现继承
原理和ES5的继承一样,实质上是先创造子类的实例对象this,然后将父类的方法添加到this上。ES6的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this
优点: 语法简单易懂,操作方便。
缺点:存在浏览器不支持ES6语法。
2.3 多态
js的多态:
子类对父类的方法进行重写,调用这个方法时,会默认执行子类的方法,即实现多态。不同子类这行父类的同名方法会有不同的结果,除了子类重写父类的方法外,还有函数参数个数,和参数类型的多态。
function Person() {Person.prototype.initfun = function() {this.init()}}function Sun1 () {this.init = function() {console.log('1')}}function Sun2() {this.init = function() {console.log('2')}}Sun1.prototype = new Person()Sun2.prototype = new Person()new Sun1().initfun() // Sun1的init方法 // 1new Sun2().initfun() // Sun2的init方法 // 2
参数个数不同, 执行不同的代码
利用arguments.length判断来实现
function fun() {if (arguments.length === 0) {console.log('参数为0')} else if (arguments.length === 1) {console.log('一个参数', arguments[0])} else if (arguments.length === 2) {console.log('两个参数', arguments[0], arguments[1])}}fun()fun(1)fun(1, 2)
参数类型不同的多态
利用typeof判断argument的类型进行判断
function fun2() {if (typeof arguments[0] === 'object' && typeof arguments[1] === 'string') {console.log('类型1')}else {console.log('类型2')}}
fun2({a: 1}, 's')
fun2({a: 1}, 1)