三、面向对象
1什么是对象
Everything is object (万物皆对象)
2什么是面向对象
面向对象不是新的东西,它只是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维护性。
面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。
因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
面向对象的特性:
-
封装性
-
继承性
-
[多态性]
面向对象的设计思想是:
-
抽象出 Class
-
根据 Class 创建 Instance
-
指挥 Instance 得结果
创建对象 简单方式 我们可以直接通过 new Object() 创建:
var person = new Object()
person.name = 'Jack'
person.age = 18
person.sayName = function () {
console.log(this.name)
}
每次创建通过 new Object() 比较麻烦,所以可以通过它的简写形式对象字面量来创建:
var person = {
name: 'Jack',
age: 18,
sayName: function () {
console.log(this.name)
}
}
对于上面的写法固然没有问题,但是假如我们要生成两个 person 实例对象呢?
var person1 = {
name: 'Jack',
age: 18,
sayName: function () {
console.log(this.name)
}
}
var person2 = {
name: 'Mike',
age: 16,
sayName: function () {
console.log(this.name)
}
}
通过上面的代码我们不难看出,这样写的代码太过冗余,重复性太高。
简单方式的改进:工厂函数 我们可以写一个函数,解决代码重复问题:
function createPerson (name, age) {
return {
name: name,
age: age,
sayName: function () {
console.log(this.name)
}
}
}
然后生成实例对象:
var p1 = createPerson('Jack', 18)
var p2 = createPerson('Mike', 18)
这样封装确实爽多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题, 但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
构造函数 更优雅的工厂函数:构造函数 一种更优雅的工厂函数就是下面这样,构造函数:
function Person (name, age) {
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
}
var p1 = new Person('Jack', 18)
p1.sayName() // => Jack
var p2 = new Person('Mike', 23)
p2.sayName() // => Mike
解析构造函数代码的执行 在上面的示例中,Person() 函数取代了 createPerson() 函数,但是实现效果是一样的。 这是为什么呢?
我们注意到,Person() 中的代码与 createPerson() 有以下几点不同之处:
没有显示的创建对象 直接将属性和方法赋给了 this 对象 没有 return 语句 函数名使用的是大写的 Person 而要创建 Person 实例,则必须使用 new 操作符。 以这种方式调用构造函数会经历以下 4 个步骤:
创建一个新对象 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象) 执行构造函数中的代码 返回新对象 下面是具体的伪代码:
function Person (name, age) {
// 当使用 new 操作符调用 Person() 的时候,实际上这里会先创建一个对象
// var instance = {}
// 然后让内部的 this 指向 instance 对象
// this = instance
// 接下来所有针对 this 的操作实际上操作的就是 instance
this.name = name
this.age = age
this.sayName = function () {
console.log(this.name)
}
// 在函数的结尾处会将 this 返回,也就是 instance
// return this
}
构造函数和实例对象的关系 使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型了。 在每一个实例对象中的proto中同时有一个 constructor 属性,该属性指向创建该实例的构造函数:
console.log(p1.constructor === Person) // => true console.log(p2.constructor === Person) // => true console.log(p1.constructor === p2.constructor) // => true
对象的 constructor 属性最初是用来标识对象类型的, 但是,如果要检测对象的类型,还是使用 instanceof 操作符更可靠一些:
console.log(p1 instanceof Person) // => true console.log(p2 instanceof Person) // => true
总结:
构造函数是根据具体的事物抽象出来的抽象模板 实例对象是根据抽象的构造函数模板得到的具体实例对象 每一个实例对象都具有一个 constructor 属性,指向创建该实例的构造函数 注意: constructor 是实例的属性的说法不严谨,具体后面的原型会讲到 可以通过实例的 constructor 属性判断实例和构造函数之间的关系 注意:这种方式不严谨,推荐使用 instanceof 操作符,后面学原型会解释为什么 构造函数的问题 使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题:
function Person (name, age) {
this.name = name
this.age = age
this.type = 'human'
this.sayHello = function () {
console.log('hello ' + this.name)
}
}
var p1 = new Person('lpz', 18)
var p2 = new Person('Jack', 16)
在该示例中,从表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。 那就是对于每一个实例对象,type 和 sayHello 都是一模一样的内容, 每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。
console.log(p1.sayHello === p2.sayHello) // => false 对于这种问题我们可以把需要共享的函数定义到构造函数外部:
function sayHello = function () {
console.log('hello ' + this.name)
}
function Person (name, age) {
this.name = name
this.age = age
this.type = 'human'
this.sayHello = sayHello
}
var p1 = new Person('lpz', 18)
var p2 = new Person('Jack', 16)
console.log(p1.sayHello === p2.sayHello) // => true
这样确实可以了,但是如果有多个需要共享的函数的话就会造成全局命名空间冲突的问题。
你肯定想到了可以把多个函数放到一个对象中用来避免全局命名空间冲突的问题:
var fns = {
sayHello: function () {
console.log('hello ' + this.name)
},
sayAge: function () {
console.log(this.age)
}
}
function Person (name, age) {
this.name = name
this.age = age
this.type = 'human'
this.sayHello = fns.sayHello
this.sayAge = fns.sayAge
}
var p1 = new Person('lpz', 18)
var p2 = new Person('Jack', 16)
console.log(p1.sayHello === p2.sayHello) // => true
console.log(p1.sayAge === p2.sayAge) // => true
四、原型和原型链
1、原型:
JS中每一个函数都有一个prototype属性,这个属性指向函数的原型对象,每一个由原型对象派生的子对象,都有相同的属性。子对象就叫构造函数,从实例原型中获取相同的属性。
function Person(age) {
this.age = age
}
Person.prototype.name = 'kavin'
var person1 = new Person()
var person2 = new Person()
console.log(person1.name) //kavin
console.log(person2.name) //kavin
2、 _Proto _
它是每一个子对象(除null外)都会有的一个属性,指向该对象的原型
function Person() {
}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true
3、构造函数constructor
每个原型都有一个constructor属性,指向该关联的构造函数
function Person() {}
console.log(Person===Person.prototype.constructor) //true
function Person() {}
var person = new Person();
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:person.constructor === Person.prototype.constructor
4、实例与原型
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
function Person() {
}
Person.prototype.name = 'Kevin';
var person = new Person();
person.name = 'Daisy';
console.log(person.name) // Daisy
delete person.name;
console.log(person.name) // Kevin
我们给实例对象 person 添加了 name 属性,当我们打印 person.name 的时候,结果自然为 Daisy。 但是当我们删除了 person 的 name 属性时,读取 person.name,从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.proto ,也就是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 Kevin。
5、原型的原型
在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是:
var obj = new Object();
obj.name = ‘Kevin’
console.log(obj.name) // Kevin
在这个例子中,我们给实例对象 person 添加了 name 属性,当我们打印 person.name 的时候,结果自然为 Daisy。 但是当我们删除了 person 的 name 属性时,读取 person.name,从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.proto ,也就是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 Kevin。 但是万一还没有找到呢?原型的原型又是什么呢?
其实原型对象就是通过 Object 构造函数生成的,结合之前所讲,实例的 proto 指向构造函数的 prototype ,所以我们再更新下关系图:
六、原型链:
最后再总结一下构造函数、原型和实例的关系:
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。——摘自《javascript高级程序设计》
其实简单来说,就是上述四-五的过程。 继上述五中所说,那 Object.prototype 的原型呢? console.log(Object.prototype.proto === null) // true 引用阮一峰老师的 《undefined与null的区别》 就是: null 表示“没有对象”,即该处不应该有值。 所以 Object.prototype.proto 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思。 所以查找属性的时候查到 Object.prototype 就可以停止查找了。
https://www.cnblogs.com/loveyaxin/p/11151586.html
五、继承
1.原型链继承
让一个构造函数的原型是另一个类型的实例,那么这个构造函数new出来的实例就具有该实例的属性。
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。
function Parent() {
this.isShow = true
this.info = {
name: "mjy",
age: 18,
};
}
Parent.prototype.getInfo = function() {
console.log(this.info);
console.log(this.isShow);
}
function Child() {};
Child.prototype = new Parent();
let Child1 = new Child();
Child1.info.gender = "男";
Child1.getInfo(); // {name: 'mjy', age: 18, gender: '男'} ture
let child2 = new Child();
child2.isShow = false
console.log(child2.info.gender) // 男
child2.getInfo(); // {name: 'mjy', age: 18, gender: '男'} false
优点:写法方便简洁,容易理解。
缺点:对象实例共享所有继承的属性和方法。传教子类型实例的时候,不能传递参数,因为这个对象是一次性创建的(没办法定制化)。
2.借用构造函数继承
function Parent(gender) {
this.info = {
name: "yhd",
age: 19,
gender: gender
}
}
function Child(gender) {
Parent.call(this, gender)
}
let child1 = new Child('男');
child1.info.nickname = 'xiaoma'
console.log(child1.info);
let child2 = new Child('女');
console.log(child2.info);
在子类型构造函数的内部调用父类型构造函数;使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上。
优点:解决了原型链实现继承的不能传参的问题和父类的原型共享的问题。
缺点:借用构造函数的缺点是方法都在构造函数中定义,因此无法实现函数复用。在父类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
3.组合继承(经典继承)
将 原型链 和 借用构造函数 的组合到一块。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性
function Person(gender) {
console.log('执行次数');
this.info = {
name: "mjy",
age: 19,
gender: gender
}
}
Person.prototype.getInfo = function () { // 使用原型链继承原型上的属性和方法
console.log(this.info.name, this.info.age)
}
function Child(gender) {
Person.call(this, gender) // 使用构造函数法传递参数
}
Child.prototype = new Person()
let child1 = new Child('男');
child1.info.nickname = 'xiaoma'
child1.getInfo()
console.log(child1.info);
let child2 = new Child('女');
console.log(child2.info);
优点: 解决了原型链继承和借用构造函数继承造成的影响。
缺点: 无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部
4.原型式继承
方法一:
借用构造函数在一个函数A内部创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的一个新实例。
本质上,函数A是对传入的对象执行了一次浅复制。
function createObject(obj) {
function Fun() {}
Fun.prototype = obj
return new Fun()
}
let person = {
name: 'mjy',
age: 18,
hoby: ['唱', '跳'],
showName() {
console.log('my name is:', this.name)
}
}
let child1 = createObject(person)
child1.name = 'xxxy'
child1.hoby.push('rap')
let child2 = createObject(person)
console.log(child1)
console.log(child2)
console.log(person.hoby) // ['唱', '跳', 'rap']
方法二:Object.create()
Object.create() 是把现有对象的属性,挂到新建对象的原型上,新建对象为空对象
ECMAScript 5通过增加Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(第二个可选)。在只有一个参数时,Object.create()与这里的函数A方法效果相同。
let person = {
name: 'mjy',
age: 19,
hoby: ['唱', '跳'],
showName() {
console.log('my name is: ', this.name)
}
}
let child1 = Object.create(person)
child1.name = 'xxt'
child1.hoby.push('rap')
let child2 = Object.create(person)
console.log(child1)
console.log(child2)
console.log(person.hoby) // ['唱', '跳', 'rap']
优点是:不需要单独创建构造函数。
缺点是:属性中包含的引用值始终会在相关对象间共享,子类实例不能向父类传参
5.寄生式继承
寄生式继承的思路与(寄生) 原型式继承
和 工厂模式
似, 即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
function objectCopy(obj) {
function Fun() { };
Fun.prototype = obj;
return new Fun();
}
function createAnother(obj) {
let clone = objectCopy(obj);
clone.showName = function () {
console.log('my name is:', this.name);
};
return clone;
}
let person = {
name: "mjy",
age: 18,
hoby: ['唱', '跳']
}
let child1 = createAnother(person);
child1.hoby.push("rap");
console.log(child1.hoby); // ['唱', '跳', 'rap']
child1.showName(); // my name is: mjy
let child2 = createAnother(person);
console.log(child2.hoby); // ['唱', '跳', 'rap']
优点:写法简单,不需要单独创建构造函数。
缺点:通过寄生式继承给对象添加函数会导致函数难以重用。使用寄生式继承来为对象添加函数, 会由于不能做到函数复用而降低效率;这一点与构造函数模式类似.
6.寄生组合式继承
前面讲过,组合继承是常用的经典继承模式,不过,组合继承最大的问题就是无论什么情况下,都会调用两次父类构造函数;一次是在创建子类型的时候,一次是在子类型的构造函数内部。寄生组合继承就是为了降低父类构造函数的开销而实现的。
通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function objectCopy(obj) {
function Fun() { };
Fun.prototype = obj;
return new Fun();
}
function inheritPrototype(child, parent) {
let prototype = objectCopy(parent.prototype);
prototype.constructor = child;
Child.prototype = prototype;
}
function Parent(name) {
this.name = name;
this.hoby = ['唱', '跳']
}
Parent.prototype.showName = function () {
console.log('my name is:', this.name);
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
inheritPrototype(Child, Parent);
Child.prototype.showAge = function () {
console.log('my age is:', this.age);
}
let child1 = new Child("mjy", 18);
child1.showAge(); // 18
child1.showName(); // mjy
child1.hoby.push("rap");
console.log(child1.hoby); // ['唱', '跳', 'rap']
let child2 = new Child("yl", 18);
child2.showAge(); // 18
child2.showName(); // yl
console.log(child2.hoby); // ['唱', '跳']
优点是:高效率只调用一次父构造函数,并且因此避免了在子原型上面创建不必要,多余的属性。与此同时,原型链还能保持不变;
缺点是:代码复杂
7.ES6、Class实现继承
原理ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。 ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this
class Animal {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayName() {
return this.name
}
sayAge() {
return this.age
}
}
class Dog extends Animal {
constructor(name, age, color) {
super(name, age)
// Animal.call(this.name,age)
this.color = color
}
sayColor() {
return `${this.name}今年${this.age}岁了,它的颜色是${this.color}`
}
//重写父类的方法
getsay() {
return this.name + super.sayAge() + this.color
}
}
let dog = new Dog('大黄', 10, 'black')
console.log(dog.sayName()); //子类继承父类的方法
console.log(dog.sayColor()); //子类自己的方法
优点:语法简单易懂,操作更方便。