参考连接:https://blog.csdn.net/weixin_33739541/article/details/91419021 自己再过一遍,方便理解和记忆,不喜勿喷
为什么要用面向对象思想编程?
大家想想,我们在搭建静态页面编写DOM元素样式的时候,是不是用了CSS类名来抽象出一类的样式?那在写JS的时候,有时候为了不让代码重复,是不是也是习惯性地抽离公共方法来复用代码?其实使用面向对象思想编程的好处也同以上,就是为了在一堆有许多共同功能的“元素”中抽离“共同点”,从而达到一劳永逸的作用。面向对象编程的好处主要有:
- 代码冗余度底
- 代码复用性高
- 高内聚低耦合
使用场景,举个栗子:
假如有一个需求,要动态创建100个一样样式和功能的div,大家一般拿到这个需求,可能就直接按部就班打上了:
for(let i = 0;i < 100;i++){ var div = document.createElement("div"); document.body.appendChild(div); div.style.width = "100px"; div.style.height = "100px"; div.style.border = "1px solid #000"; div.innerHTML = i + 1; div.style.textAlign = "center"; div.style.lineHeight = "100px"; }
还好这个需求是100个“一样的”,要是让你创建100个大小一样,颜色不一样的div,并且每个div点击下去,都会变颜色,每个div鼠标移动上去都会变大一倍等等等等复杂的需求,你要怎么做?你可能会开始编写一个好用的函数,传入参数,制造定制化的div,return出来,这也确实是一个好办法。如果使用“面向对象”来做,要怎么搞?看完这篇文章,你也许就懂了。
面向对象,首先要有对象
ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”
我们经常使用字面量的方法创建对象:
var person = { name:"tom", age:29, job:"coding", sayName:function(){ alert(this.name) } }
知识延伸,可跳过:
如果要删除对象中的某个属性可以用delete操作符 delete person.name
,使用for-in
循环这个对象,可以返回每个属性,可以对每个属性进行修改:person.name = ‘cc‘
,访问person.name
时,返回‘cc‘
。对象之所以拥有这些功能,是因为ECMA对于对象的规定。如果你想打破这默认的规定,比如:你想保护这个属性,让它不可写,不可删,可以使用ECMAScript 5 的 Object.defineProperty()
方法。
Object.defineProperty(person,‘name‘,{//此代码执行一次 再更改里面的属性就失效了,也就是只生效第一次 configurable: false, // 不可用delete删除 enumerable: false, // 不可在for-in中返回 writable: false, // 不可修改 //不能与get/set属性共存 value: "tom", // 读、写的位置 //不能与get/set属性共存 get: function(){ // 读name属性的时候调用 console.log("get"); return this.value; }, set: function(newValue){ // 写name属性的时候调用 console.log("set"); if (newValue === ‘cc‘) { this.value = newValue; this.age += 1; } } }) // delete person.name; //执行删除时会报错 for(let key in person){//没有枚举出name属性 console.log(key); } person.name = ‘cc‘;//报错 console.log(person);
ps: getter和setter函数不是必须的,也无需成对出现,但是只有getter的话,属性将不可写入,只有setter的话,属性将不可读(非严格模式返回undefined
),所以还是成对儿吧~此外,定义多个属性ES5还提供了一个Object.defineProperties()
方法, Object.getOwnPropertyDescriptor()
这个方法可以读取这些配置。
--------------------------------------------------------------------
回归正题,用上面的方法创建对象的时候,每创建一个对象,就得写以上一堆代码,这些代码又是相似的,对于同一类的对象(属性相同),这样做确实很蠢,所以总是要偷懒的~ 包装一下呗:
function createPerson(name,age,job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function(){ alert(this.name) } return o; } var person1 = createPerson("tom",18,‘coding‘); var person2 = createPerson("bob",19,‘log‘);
这便是设计模式中的 工厂模式,这虽然解决了创建多个相似对象的问题,但是还是难以将“同类”的概念抽离出来,用工厂模式创建的对象,构造函数都是Object,即:
console.log(person1.constructor === Object);//true console.log(person1 instanceof Object);//true
为了更加明确创建一个类型为“人类”的对象,便有了构造函数模式,设计一个“人类”构造器,由它构造出来的“实例”就是一个个的人:
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = function(){ alert(this.name); }; } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor");
相比较工厂模式
创建对象,构造函数模式:
- 没有显式地创建对象
- 直接将属性和方法赋给了 this 对象
- 没有 return 语句
- 换成了new 操作符调用它
那为什么用构造函数可以达到工厂模式一样创建对象的效果呢?因为new
操作符起到了至关重要的作用,用new操作符经历了:
- (1) 创建一个新对象;
- (2) 将构造函数的作用域赋给新对象(也就是把 this 指向这个新对象);
- (3) 执行构造函数中的代码(为这个新对象添加属性);
- (4) 返回新对象。
new
操作符帮你隐式做到了工厂模式的功能,而且,每个用new
操作符实例化出来的实例:
person1.constructor === Person // true person1 instanceof Person // true person2.constructor === Person // true person2 instanceof Person // true
这样,是不是更加明确了person1 和 person2 这俩货就是“Person”的实例,就是“人类”,而不是模糊的“Object”。
目前为止,相对于开篇的字面量一个个去创建对象有了很大的进步,但是呢,凡是总有但是,构造函数模式还有缺陷,大家有没有发现,每次使用调用Person构造函数的时候,sayName
方法都要在每个实例上重新创建一遍,这也太蠢,不仅仅浪费了内存,代码上还多余,如何解决?达到一劳永逸的效果,这时候原型模式就来了。
还有一种办法,就是把公共的函数抽出来写在全局下,但是这样做,全局函可以在任何地方访问到,就无法变成Person的自有函数,而且有很多这样的函数的话会污染全局作用域。
什么是原型?它是函数的一个属性,函数在创建的时候就会有prototype
(原型)属性,这个属性就是该构造函数创建出来的实例的原型对象。啥意思呢,就以上代码来说:person1
的原型对象就是Person.prototype
。prototype
属性上都有哪些东西呢?打印出来就知道了嘛,在chome浏览器控制台打印如下:
可以看到,默认就有constructor
和__proto__
这俩货,constructor
是一个函数,可以看到它其实就是fn
函数,红皮书上说:所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针
Person.prototype.constructor === Person // true Person.constructor === Function // true 因为Person.__proto__.constructor === Function 为true,它其实是在原型链上的constructor属性
还有一个__proto__属性([[Prototype]]),这个就很重要了,它就是原型模式的原理,它是一个指针,指向构造函数的原型对象,也就是说,图片上的fn.prototype.__proto__
指向了fn的构造函数的原型对象,而fn的构造函数是Function,Function的原型对象是Object.prototype
,所以,fn.prototype.__proto__ === Object.prototype
为true,这里再拓展一下,原型链的顶层是null,Object.prototype.__proto__ === null
为true。这样描述好像很绕和很抽象,拿Person 与 person1、person2的关系,可以有这样的图表示:
通俗说,person1和person2都是Person new
出来的,所以,person1和person2可以访问到Person
的prototype
上的所有属性,如何访问到呢?就是通过person1.__proto__
和person2.__proto__
,他们指向Person
的prototype
person1.__proto__ === Person.prototype // true person2.__proto__ === Person.prototype // true
// 原型模式 function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person();
so, person1.__proto__.name === ‘Nicholas‘ // true
,省略掉中间的__proto__
,也可以访问得到a,person1.name === ‘Nicholas‘ // true
,利用构造函数原型属性,在原型属性上添加属性和方法,就可以让该构造函数的实例们共享这些属性和方法了。js在访问一个对象的某个属性是,先从该对象自身搜索,自身搜索结果为undefined
时,再从自身的__proto__
属性上找,再为undefined
时,再从自身的__proto__
属性的__proto__
属性上找,一直这样下去,直到null位置,这写就构成了原型链。总结一句话:当找某个对象的属性时,先找自身,再通过原型链一层层往上找。
所有属性都挂载在原型上也不行,这样实例没有差异性,除非每个实例都添加自身属性,从而覆盖它原型链上的属性,组合使用构造函数模式和原型模式就可以解决这样的问题,也是在 ECMAScript中使用最广泛、认同度最高的一种创建自 定义类型的方法
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.friends = ["Shelby", "Court"]; } Person.prototype = { constructor : Person, sayName : function(){ console.log(this.name); } } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor"); person1.friends.push("Van"); console.log(person1.friends); //"Shelby,Count,Van" console.log(person2.friends); //"Shelby,Count" console.log(person1.friends === person2.friends); //false console.log(person1.sayName === person2.sayName); //true
这样就既实现了实例的差异性,又实现了代码的复用。
以上创建对象的方法,讲了五种:字面量创建、工厂模式、构造函数模式、原型模式、构造函数与原型的组合模式, 除了字面量创建是直接创建出一个对象,其余的几种,都是通过调用函数(实例化)来创建对象(实例)的,无论通过普通函数也好,通过构造函数也好(普通函数前面加个new来调用就是构造函数啦),我的理解是,除了字面量创建,其他的几种方法都是抽象出一种“类”,利用“类”实例化出各个对象的面向对象的编程思想。
那我们创建创建的一个个的“类”(构造函数),可以互相调用,来复用代码么?可以,这就是继承。
js 通过原型链实现“类”(构造函数)之间的继承
在上面的介绍中,我们知道实例与构造函数的关系:构造函数(构造函数A)实例化一个实例后,实例(实例a)就会拥有构造函数prototype
属性上的所有东西,通过自己的__proto__
指针指向这些东西。那,同样的,如果这个构造函数(构造函数A)的prototype
属性也是另一个构造函数(构造函数B)的实例,A的prototype
属性也会利用自己的__proto__
指针把Bprototype
上的所有东西捞过来,这可以干嘛呢?这就可以让实例a拥有Bprototype
上的所有东西了,如何拥有?先通过自己的__proto__
读构造函数A的prototype
,构造函数A的prototype
再通过它的__proto__
读构造函数B的prototype
,再返回传递给实例a。这一层层向上读取属性,就是原型链(实例与原型的链条)。说来说去,还是有点晕啊,来段代码:
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } //继承了 SuperType,这样SubType.prototype上就有property这个属性和getSuperValue这个方法,传递给SubType的实例instance // SubType.prototype.property -> true // SubType.prototype.getSuperValue -> function(){return this.property;} SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function (){ return this.subproperty; }; var instance = new SubType(); console.log(instance.getSuperValue()); //true
调用instance.getSuperValue()
会经历三个搜索步骤:
- 1)搜索实例;
- 2)搜索 SubType.prototype ;
- 3)搜索 SuperType.prototype。 原型链继承的原理就是:利用原型让一个引用类型继承另一个引用类型的属性和方法,如何实现“利用原型让一个引用类型继承另一个引用类型的属性和方法”?就是让一个引用类型的原型属性成为另一个引用类型的实例,示例代码中:
SubType
的原型不仅具有作为一个SuperType
的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了SuperType
的原型。最终实现的结果就是:instance
指向SubType
的原型, SubType 的原型又指向SuperType
的原型。
ps: 所有引用类型默认都继承了 Object ,而这个继承也是通过原型链实现的。所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype。 啥意思?就是任何一个函数fn.prototype.__proto__ === Object.prototype
为true
,js内部就是通过原型继承来实现的,让fn.prototype = new Object()
,这也正是所有自定义类型都会继承 toString() 、valueOf()
等默认方法的根本原因。哈哈,其实没有可以去写原型链继承,在开发时,不经意间就用上了继承。
如何判断一个实例是否是某个构造函数的实例,但是由于原型链的关系,我们可以说 instance 是 Object 、 SuperType 或 SubType 中任何一个类型的实例。instanceof
和 isPrototypeOf
都可以用来判断。原型链继承其实有一个毛病,大家可以看到,原型链继承是在原型上放想要继承的对象的实例,那全部属性和方法都是放在prototype
原型上,则这些属性和方法又会被它自己的实例们所共享,如果有一个属性是引用类型,有一个实例去改变了这个属性,那所有实例获得到的属性都是被改变后的了;还有一个问题,都放在原型上的话,所有实例们所读取的值都一样一样的,做不了差异性。那怎么办呢,我们在想,能不能把需要有差异性的属性在自身属性中继承,而不是放在原型中去继承?借用 构造函数和原型链的组合继承 可以做到。
function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ console.log(this.name); }; function SubType(name, age){ //继承属性(利用构造函数) SuperType.call(this, name); this.age = age; } //继承方法(原型链继承),其实这里也会把属性挂载到原型上,和上面的利用构造函数可以说是有“重复属性” SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ console.log(this.age); }; var instance1 = new SubType("Nicholas", 29); instance1.colors.push("black"); console.log(instance1.colors); //"red,blue,green,black" instance1.sayName(); //"Nicholas"; instance1.sayAge(); //29 var instance2 = new SubType("Greg", 27); console.log(instance2.colors); //"red,blue,green" instance2.sayName(); //"Greg"; instance2.sayAge(); //27
如上代码,我们可以利用call或者apply来调用想要继承的构造函数,把this指给自身,这样,继承者自身就拥有了被继承者的自身的属性,相比之前,将属性“正大光明”的继承在了自身属性中,而不是在prototype中“偷偷”继承。其他还是一样写。以上的继承方式是JS中最常用的方式。不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数,也就是说,以上例子:有两组 name
和colors
属性:一组在实例上,一组在 SubType
原型中。所以如果要改进,就需要把两次调用SuperType
构造函数变成一次调用:
function inheritPrototype(subType, superType){ var prototype = Object(superType.prototype); //创建对象 prototype.constructor = subType; //增强对象 subType.prototype = prototype; //指定对象 } function SuperType(name){ this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function(){ console.log(this.name); }; function SubType(name, age){ SuperType.call(this, name); this.age = age; } inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function(){ console.log(this.age); }
以上就是寄生组合式继承,这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType. prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf() 。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
ECMAScript6 中,对于“类”有了新的语法糖,实质还是基于原型链的原理:
class PersonClass { // 等价于 PersonType 构造器 constructor(name) { this.name = name; } // 等价于 PersonType.prototype.sayName sayName() { console.log(this.name); } // 等价于 PersonType.create static create(name) { // 静态成员 return new PersonClass(name); } } let person = new PersonClass("Nicholas"); person.sayName(); // 输出 "Nicholas" console.log(person instanceof PersonClass); // true console.log(person instanceof Object); // true console.log(typeof PersonClass); // "function" console.log(typeof PersonClass.prototype.sayName); // "function"
使用是要注意:
- 类声明不会被提升,与let声明相似
- 类声明中的所有代码自动运行在严格严格模式下
- 类的所所有方法都不可枚举(不能不用for in 遍历到)
- 类中的方法不能用new调用(内部都没有 [[Construct]])
ES6继承:
class Rectangle { constructor(length, width) { this.length = length; this.width = width; } getArea() { return this.length * this.width; } } class Square extends Rectangle { constructor(length) { // 与 Rectangle.call(this, length, length) 相同 super(length, length); } } var square = new Square(3); console.log(square.getArea()); // 9 console.log(square instanceof Square); // true console.log(square instanceof Rectangle); // true
注意:继承了其他类的类被称为派生类( derived classes )。如果派生类指定了构造器,就需要 使用 super() ,否则会造成错误。若你选择不使用构造器, super() 方法会被自动调用, 并会使用创建新实例时提供的所有参数。继承可以把基类的静态方法直接继承下来!
完!
。