原型的优点与缺点
优点:共享
缺点:原型中所有属性是被很多实例共享的,共享对于函数非常合适,对于包含基本值的属性也还可以。但如果属性包含引用类型,就存在一定的问题:基本类型的值和引用类型的值在内存空间的存放方式
工厂模式
缺点:每次new一个工厂都要创建Object这样会造成资源的浪费,并且new出来的工厂无法识别归属于哪个对象
优点:工厂模式解决了重复实例化的问题,
构造函数模式
function Box(name, age){ //构造函数模式 this.name = name; this.age = age; this.run = function() { return this.name + this.age ; } } var box1 = new Box('Lee', 100) //new Box()即可 var box2 = new Box('Jack', 200); console.log(box1.run()) console.log(box1 instanceof Box); //true, 很清晰的识别其从属于Box console.log(box1.run == box2.run) //false, 不共享
缺点:这里的run()就只是做些简单的功能,没有必要多次初始化。
优点: 解决对象识别的问题。
原型模式创建对象
function Box() {} Box.prototype = { //使用字面量的方式,当然也可单个设置,如:Box.propotype.age = ... constructor: Box, age: 100, family: ['father', 'mother'], // 添加一个数组属性 run: function() { return this.age + this.family; } } var box1 = new Box(); box1.age = 200; // 值类型,只在box1中更改,box2中没变 box1.family.push('brother'); // 引用类型,在实例中添加元素 console.log(box1.run()); // 200father,mother,brother var box2 = new Box(); console.log(box2.run()); // 100father,mother,brother,引用类型可能会造成数据被修改
原型模式下的属性和方法都是共享的,所以属性的值可以随便改,所有失去了独立性。
为了解决构造传参和共享问题,可以用---组合构造函数+原型模式
需要独立的部分用构造函数,需要共享的部分用原型函数
function Box(name, age) { // 不共享的内容使用构造函数 this.name =name; this.age = age; this.family = ['father', 'mother']; } Box.prototype = { // 共享的内容,使用原型模式 constructor: Box, run: function() { return this.name + this.age + this.family; } } var box1 = new Box('zhangsan', 20); box1.family.push('brother'); // 引用类型,在实例中添加元素 console.log(box1.run()); // zhangsan20father,mother,brother var box2 = new Box('lisi',30); console.log(box2.run()); // lisi30father,mother,引用类型也保持了自己的独立性
缺点:不能体现封装 性,不管你是否调用了原型中的共享方法,它都会初始化原型中的方法,并且在声明一个对象时,构造函数+原型部分让人感觉又很怪异,最好就是把构造函数和原型封装到一起
为了解决这个问题,我们可以使用 动态原型模式
当第一次调用构造函数时,run()方法发现不存在,然后初始化原型。当第二次调用,就不会初始化,并且第二次创建新对象,原型也不会再初始化了。这样及得到了封装,又实现了原型方法共享,并且属性都保持独立
以上讲解了各种方式对象创建的方法,如果这几种方式都不能满足需求,可以使用开始那种模式:
寄生构造函数=>工厂模式+构造函数模式。
缺陷:面相对象强调对象的属性私有,而对象的方法是共享的。而上面的工厂方法创建对象的时候要为每个 对象创建各自私有的方法。同时由于为每个对象都创建逻辑相同的方法,浪费内存
解决办法:把函数放在外面,里面只是一个名
这种模式比较通用,但不能确定对象关系,所以,在可以使用之前所说的模式时,不建议使用此模式
寄生构造模式与工厂模式的区别
作用:用意是要在不扩展原生构造函数的情况下自定义一个扩展型的构造函数。寄生构造函数专门用来为js原生的构造函数定义新的方法。尽管给原生的内置引用类型添加方法使用起来特别方便,但我们不推荐使用这种方法。因为它可能会导致命名冲突,不利于代码维护。
在一些安全的环境中,比如禁止使用 this 和 new,这里的 this 是构造函数里不使用 this ,这里的 new 是在外部实例化构造函数时不使用 new。这种创建方式叫做稳妥构造函数
稳妥构造函数和寄生类似。就是为了在安全环境下使用,三种模式都不能确定所创建对象的类型
由于添加方法时去掉了"this",即使修改所创建对象的属性:Box.name = "Greg";
也可以通过run方法访问创建对象时的原始数据:box.run(); //name值不变
关于原型创建对象的注意
- 不可以再使用字面量的方式重写原型,因为会切断实例和新原型之间的联系
- 如果构造函数内有return语句,且返回一个指定对象,那么将返回这个对象,否则不管return语句,返回this对象
- 如果对普通函数(内部没有this关键字的函数)使用new,将返回一个空对象
- new命令的执行原理:
1:创建一个空对象,作为将要返回的对象实例
2:将这个空对象的原型,指向构造函数的prototype属性
3:将这个空对象赋值给函数内部的this关键字
4:开始执行构造函数内部的代码
原型链继承
继承的含义及作用:继承是面向对象中一个比较核心的概念。其他正统面向对象语言都会用两种方式实现继承:一个是接口实现,一个是继承。而 ECMAScript 只支持继承,不支持接口实现,而实现继承的方式依靠原型链完成。
缺点:引用共享和父类无法传参的问题。适合用来继承方法,不适合用来继承属性,我们希望每个属性独立
借用构造函数继承(对象冒充)
我们采用一种叫借用构造函数的技术,或者成为对象冒充(伪造对象、经典继承)的技术来解决这两种问题。
缺点:
1:对象冒充只能继承对象实例中的属性,无法继承原型上的属性。
2:方法都在构造函数中没有原型,复用无从谈起
3:无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
4:实例并不是父类的实例,只是子类的实例
优点:
1:各个实例的属性值是独立的,包括引用类型:
2:解决了原型链继承中,子类实例共享父类引用属性
3:创建子类实例时,可以向父类传递参数
组合继承(原型链 + 借用构造函数)-最常用
借用构造函数虽然解决了刚才两种问题,但没有原型,方法共享则无从谈起。所以,我们需要 原型链 + 借用构造函数的模式,这种模式成为组合继承
优点:
1.弥补了方式构造继承的缺陷,可以继承实例属性
也可继承原型上的属性
2.既是子类的实例,也是父类的实例
3.不存在引用属性共享问题
4.可传参
5.函数可复用
6.仅仅多消耗了一点内存,性能好。
缺点:
1:调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了
原型式继承
原型式继承与原型链继承的原理基本一样,只是函数的结构改变
可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制,而复制得到的副本还可以得到进一步增强,添加实例属性,
寄生式继承(原型式+工厂模式)
优点:封装了创建对象的过程
缺点:引用类型还是被共享
原理:与原型式继承非常类似,也是基于某个对象或某些信息创建一个对象,然后增强对象,添加实例属性最后返回对象。
寄生组合继承
组合式继承是 JavaScript 最常用的继承模式;但组合式继承也有一点小问题,就是父类构造函数在使用过程中会被调用两次:一次是创建子类型的时候,另一次是在子类型构造函数的内部。
优点:解决了组合继承中两次调用调用构造函数的问题。集寄生式继承和组合继承的优点于一身是实现基于类型继承的最有效方式。
缺点:过程复杂代码冗余