谈谈创建对象实例
尽管Object构造函数、对象字面量都能够用来创立单个对象,但这些形式有个显著的毛病,创立多个对象的时候,会产生大量的反复代码,造成代码冗余。
//对象字面量var person1 = { name:'123'}//Object构造函数var person2 = new Object()person2.name = '123'
最直观的能够利用函数一次申明屡次调用的个性解决代码冗余,工厂模式次要是在函数体内通过Object构造函数或对象字面量新建一个对象,给对象赋属性,最初返回这个新建的对象。工厂模式的毛病是没有对象辨认,即无奈得悉新建对象的类型。
function createPerson(name){ var o = new Object(); o.name = name; o.sayName = function(){alert(this.name)}; return o;}var person1 = createPerson('JACK');
构造函数模式是JS中创立特定类型对象自带的形式,有很强的类型辨认,次要通过new关键字来实现一个构造函数对象的创立。
function Person(name){ this.name = name; this.constructor = Person; this.sayName = function(){alert(this.name)};}var person2 = new Person('JACK')
但构造函数的申明的办法每次被调用时都要在每个实例上从新创立一遍,不同实例下的同名函数是不相等的,其本质是Function类创立了不同的实例。
function Person(name){ this.sayName = new Function("alert(this.name)");}
能够把函数的定义转移到构造函数内部,而在构造函数外部指向内部的函数,就造成了创立对象构造函数的优化版本。
function Person(name) { this.name = name; this.getName = getName;}function getName() { console.log(this.name);}var person1 = new Person('kevin');
这样做的确解决了两个函数做同一件事的问题,可是新问题又来了,在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点徒有虚名。如果对象须要定义很多办法,那么就要定义很多个全局函数,于是这个自定义的援用类型就丝毫没有封装性可言了。函数不管放在构造函数外面还是放在构造函数里面都不是很好的解决形式,那何不再创立一个对象,专门搁置实例专用的办法和属性,实例通过一种形式主动索引到这么私有的属性和办法,这就是原型模式,JS中申明一个函数对象时会生成一个prototype属性——原型对象,每个由构造函数产生的实例的隐式原型__proto__指向该原型对象,因而实现每个实例能够拜访私有的属性和办法。
function Person(){}Person.prototype.name = '123'Person.prototype.sayName = function(){alert(this.name)}var person3 = new Person()person3.sayName() //'123'
该当留神的是如果重写了Person.prototype对象,就是等于切断了构造函数与其原型之间的分割。
function Person(){}Person.prototype = { name:'123', sayName:function(){alert(this.name)}}var person = new Person();person.sayName()//123var a = Person.prototype.constuctor === Person //false
依据之前创立一个函数的同时会创立它的prototype对象,这个对象也会主动取得constructor属性,重写了Person.prototype对象后也销毁了constructor属性,而是从原型链上持续寻找,由上图能够看到找到的是Object,能够让constructor从新指向Person。
function Person(){}var friend = new Person();Person.prototype = { constructor: "Person", name:'123' sayName:function(){alert(this.name)}}friend.sayName()
原型模式解决了实例办法放在哪里的问题,然而也会附带放入独特的属性,这些属性如果是援用类型,实例中又容许批改原型上数据,那么任何一个实例中的数据都变得不牢靠,一旦批改会在其余所有实例上体现。因而JS创建对象的准则是实例个别要有属于本人的全副属性,而办法则能够由原型对象那里委托拜访,回顾构造函数中能够通过this携带本身属性,于是构造函数模式和原型模式的组合形式应运而生,如下代码:
function Person(name){ this.name = name}Person.prototype = { constructor:Person, sayName: function(){alert{this.name}}}
它正当的解决了新建对象属性放在哪里,办法放在哪里的问题——在构造函数中定义属性,在原型对象上定义方法。因而每个实例都会有本人的一份实例属性的正本,但同时又共享着对办法的援用,这种形式是广泛应用的创建对象的形式。惟一美中不足的就是它们是离开,能够通过构造函数原型上是否存在该函数办法名的形式将申明在原型上的办法写在构造函数外部,写在一起的模式也叫动静原型模式。
function Person(name,age,job){ this.name = name; if(typeof this.sayName != "function"){ Person.prototype.sayName = function(){alert(this.name)} }}
还有两种用于非凡场景下的模式,在其余模式都不实用的状况下,能够应用寄生构造函数模式。它等同于new操作符的作用,即在一个对象(有时候是空白)的根底上通过其余构造函数润饰一下(原型链继承)返回这个润饰结束的对象。
function Person(name){ var o = new Object(); o.name = name; o.sayName = function(){ alert(this.name); } return o;}
除了应用new操作符并把应用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是截然不同的。这个模式能够在非凡的状况下用来为对象创立构造函数。假如咱们想创立一个具备额定办法的非凡数组。因为不能间接批改Array构造函数(最好的形式是polyfill),因而能够应用这个模式。
function objectFactory(obj,Constructor,args) { obj = obj || new Object(), obj.__proto__ = Constructor.prototype; let ret = Constructor.apply(obj, args); return typeof ret === 'object' ? ret : obj; }; var o = objectFactory(Person,'Yadang') o.sayName()//My name is Yadang
稳当构造函数模式是另一种创立稳当对象,稳当对象没有公共属性,而且其办法也不援用this的对象,稳当对象最适宜在一些平安环境中或者在避免数据被其余应用程序改变时应用。
function Person(name){ var o = new Object(); o.sayName = function(){ alert(name) } return o;}//只能应用sayName办法进行name的拜访。
ES6中的创建对象形式是模拟传统面向对象上的类,其babel实质是构造函数模式和原型模式的组合的实现,如下代码。
class Person{ constructor(name){this.name = name} sayName(){console.log("My name is "+this.name)} } var man = new Person('Yadang') man.sayName()//My name is Yadang
谈谈原型继承
内置对象实现继承次要是依附原型链继承。
function Person(){}function Children(){}Children.prototype = new Person()
原型链继承的两个问题:1,所有子实例共享一份实例。2,不能传递参数。借用构造函数继承能够解决这个问题。
function Person(){ this.name = name}function Children($name,$age){ Person.call(this,$name) this.age = $age}var instance = new Children('JACK',24)
这种模式看似解决了所有实例共享一份实例和不能传参的问题,但其实就是将要继承的父类的构造函数在子类中执行一遍,因而子类取得了父类的属性和办法,则这样也导致代码无奈复用,而且父类的定义的办法,在子类须要从新定义一遍,无奈实现构造函数的复用,每一个新实例都有父类构造函数的正本,且都只能继承父类构造函数原型上属性。原型链和借用构造函数组合继承能够将两种继承形式联合起来,从而施展二者之长的一种继承模式。其背地的思路是应用原型链实现对原型链实现对原型属性和办法的继承,而通过借用构造函数来实现对实例属性的继承。这样,即通过在原型上定义方法实现了函数复用,又可能保障每个实例都有它本人的属性。这种思路是罕用的实现继承的办法。
function Person($name){ this.name = $name; this.color = 123;}Person.prototype.sayName = function(){alert(this.name)}function Children($name,$age){ Person.call(this,$name); this.age = age;}Children.prototype = new Person()
这种形式一举解决了三个问题:1,让父类构造函数在子类构造函数中调用,子类继承了父类的属性,不仅解决了所有实例共享一份实例的问题,还解决了原型链继承中不能传参的问题。2,在父类原型上定义方法而不是在父类构造函数的办法,解决了父类办法在子类从新定义的问题。3,用原型链继承,保障继承关系。其毛病就是调用了两次父类构造函数。
然而仍然会存在问题:父类构造函数被调用了两次,Children.prototype = new Person()第一次调用的时候会产生属性name和color,Person.call(this,$name)是第二次调用,这时新产生的属性会笼罩之前产生的同名属性。
以上都是两个构造函数之间继承解决方案,其目标就是让子类的实例可能继承到父类上的属性和办法,其根底就是建设在父类构造函数存在的前提下,原型继承能够疏忽这个限度。如果一个对象只有对象字面量的值,没有构造函数,那么怎么继承这个对象上的属性和办法呢?
function object(o){ function F(){} F.prototype = o; return new F();}var children = object(person)
其外围操作就是强行将两个对象用原型链绑定在一起,等同于Object.create()。其弊病和原型链一样,蕴含援用类型值的属性始终都会共享相应的值。
寄生继承和原型继承相似,其特点是输出一个对象,输入一个继承该对象的对象。原型继承和寄生继承本质无非是在新对象上创立被继承对象的正本,仍然防止不了援用类型的属性被所有子实例共享问题。
function others(o){ var c = Object.create(o); c.sayName = function(){alert('hi')} return c}
寄生组合继承其外围操作就是强行将父类的构造函数原型对象与子类的构造函数的原型绑定在一起,寄生组合继承能够解决父类结构函数调用两次的缺点。
function inherit(Person,Children){ var prototype = object(Person.prototype) //申明一个对象,该对象继承了父类原型的属性和办法 prototype.constructor = Children Children.prototype = prototype }
ES6中的class继承次要是通过extends关键字实现两个class之间的继承。
class Person{ constructor(name,age){ this.name = name; this.age = age; } sayName(){ console.log(this.name) }}class Children extends Person{ constructor(name,age,job){ super(name,age) this.job = job } sayAge(){ console.log(this.age) }}var children = new Children('zhangsan','17','codeMan')children.sayName() children.sayAge()
super关键字在这里充当了Person.prototype.constructor.call(this)的作用。extends关键字采纳的是寄生组合继承,这样只会在父类的构造函数原型上操作,而不波及到父类实例,能够写一个extends的babel外围代码:
function _inherits(subClass, superClass) { //关键性代码 subClass.prototype = Object.create( superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } } ); }
ES6提供了新的继承写法,不仅看起来更面向对象一些还突破ES5之前是无奈继承原生构造函数的限度,呈现这种状况的实质上是原生构造函数会疏忽apply办法传入的this,也就是说,原生构造函数的this无奈绑定,导致拿不到外部的属性。
function OtherArray(){ Array.apply(this,arguments); //等同于Array.apply(null,arguments)}var array = new OtherArray()array.push(123) //TypeError: array.push is not a function
指定原型对象可不可以呢,尽管能够,然而它的后果会造成原型链的凌乱。
//强行指定原型对象function OtherArray(){}OtherArray.prototype = Array.prototypevar array = new OtherArray()array.push(123)console.log(array)//OtherArray [123]console.log(OtherArray.prototype.__proto__ === Array.prototype)//falseconsole.log(OtherArray.prototype.__proto__ === Object.prototype)//true 蹩脚,少了一个链条
回忆上文创建对象中应用寄生构造函数模式能够实现对内建对象的继承,然而须要重构原型链。
function SpecialArray(){ var values = new Array(); values.push.apply(values,arguments); values.__proto__ = SpecialArray.prototype return values}SpecialArray.prototype = Object.create(Array.prototype);Object.defineProperty(SpecialArray.prototype, "constructor", { enumerable: false, value: SpecialArray});SpecialArray.prototype.toPipedString = function() { return this.join("|");};var arr = SpecialArray()arr.push(123)//[123]
ES6容许继承原生构造函数定义子类,起因ES6的继承机制(采纳的是寄生组合模式)不同——ES6的extends调用super的时候先新建父类的实例对象this,而后再用子类的构造函数润饰this,使得父类的所有属性都能够继承。
class OtherArray extends Array { constructor(...args) { super(...args) }}var array = new OtherArray()array.push(123) //[123]