企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
1.创建自定义对象的最简单方式就是**创建一个Object()的实例**,然后再为它添加属性和方法;随后流行**使用对象字面量创建对象**。 2.ECMAScript的属性(property)由内部才能用的特性(attribute)来描述。内部特性无法在JavaScript中直接访问。为了表示特性是内部值,该规范把它们放在了两对方括号中。 3.ECMAScrip有两种属性:数据属性和访问器属性。 4.数据属性有四个描述其行为的特性: **[[Configurable]]**:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。这个特性的默认值为true。 **[[Enumearble]]**:表示能否通过for-in循环返回属性。这个特性默认值为true。 **[[Writable]]**:表示能否修改属性的值。这个特性的默认值为true。 **[[Value]]**:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined。 5.要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中描述符对象的属性必须是configurable、enumerable、writable、value。例: ~~~ var person = {}; Object.defineProperty(person,’name’,{ writable:false, value:’Nicholas’ }); ~~~ 6.一旦把属性定义为不可配置的,就不能再把它变回可配置了,此时再调用Object.defineProperty()方法修改除writable之外的特性,都会导致错误。在调用Obejct.defineProperty()方法时,如果不指定,configurable、writable、enumerable特性的默认值都为false。 7.访问器不包含数据,包含一对getter和setter函数(不过这两个函数都不是必须的)。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下4个特性: **[[Configurable]]**:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。这个特性的默认值为true。 **[[Enumearble]]**:表示能否通过for-in循环返回属性。这个特性默认值为true。 **[[Get]]**:在读取属性时调用的函数。这个特性的默认值为undefined。 **[[Set]]**:在写入属性时调用的函数。这个特性的默认值为undefined。 8.访问器属性不能直接定义,必须使用***Object.defineProperty()***来定义。例: ~~~ var book = { _year:2004, edition:1 }; Object.defineProperty(book,’year’,{ get:function(){ return this._year; }, set:function(newValue){ if (newValue>2004) { this._year = newValue; this.edition += newValue-2004; } } }); book.year = 2005; alert(book.edition); //2 ~~~ 以上代码创建了一个book对象,并给它定义了两个默认属性。_year和edition。**_year前面的下划线是一种常用的记号,用于表示只能通过对象方法访问的属性。**在这个函数里,把year属性修改为2005会导致_year变成2005,而edition变为2。**这是使用访问器属性的常见方式,即设置一个属性的值会导致其他属性发生变化。** 9.***Object.defineProperties()***方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。例: ~~~ var book = {}; Object.defineProperties(book,{ _year: { value:2004 }, edition:{ value:1 }, year:{ get:function(){ return this._year; }, set:function(newValue){ if (newValue>2004) { this._year = newValue; this.edition += newValue-2004; } } } }); ~~~ 10.***Object.getOwnPropertyDescriptor()***方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问其属性,这个对象的属性有configurable、enumerable、get和set;如果是数据属性,这个对象的属性有configurable、enumerable、writable和value。这个方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用这个方法。 ~~~ var descriptor = Object.getOwnpropertyDescriptor(book,’_year’); alert(descriptor.value) //2005 alert(descriptor.configurable) //false alert(descriptor.get) //undefined ~~~ 11.**工厂模式在JavaScript中是指用函数来封装以特定接口创建对象的细节**。这种模式抽象了创建具体对象的过程。例: ~~~ 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; } ~~~ **工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎么知道一个对象的类型)。** 12.**构造函数模式** **通过创建自定义的构造函数,从而定义自定义对象类型的属性和方法。** ~~~ function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.setName = function(){ alert(this.name) }; } ~~~ Person()函数与createPerson()函数之间的不同之处在于: -没有显式地创建对象; -直接将属性和方法赋给了this对象; -没有return语句; 按照惯例,***构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。***这个做法借鉴自其他OO语言,主要为了区别于ECMAScript中的其他函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。 要创建Person对象的新实例,必须使用new 操作符。以这种方式调用构造函数实际上会经历以下四个步骤: * **创建一个新对象;** * **将构造函数的作用域赋给新对象(因此this就指向了这个新对象)** * **执行构造函数中的代码(为这个新对象添加属性);** * **返回新对象**。 创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型。 任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数没有什么区别。**当一个原意用作构造函数的函数不通过new操作符调用,那函数体内的this对象就指向全局作用域window。**当然,可以通过call()或者apply()在某个特殊对象的作用域中调用。例: ~~~ var o = new Object(); Person.call(0,’Ken’,28,’frontend’); o.sayName(); //Ken ~~~ 构造函数的主要问题在于,每个方法都要在每个实例上重新创建一遍。可以通过把函数定义转移到构造函数外面来解决这个问题: ~~~ function sayName() { alert(this.name); } ~~~ 但这样的做法又带来了新问题,全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言。 13.**原型模式** 每个创建的**函数**都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以就让所有对象实例共享它所包含的属性和方法。例: ~~~ function Person(){ } Person.prototype.name = ‘ken’; Person.prototype.age = 28; Person.prototype.job = ‘frontend’; Person.prototype.sayName = function(){ alert(this.name); }; var person1 = new Person(); var person2 = new Person(); alert(person1.sayName == person2.sayName); //true (不加括号,否则对比的是运行值) ~~~ 关于原型对象的深入理解可以查看P148. 14.无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。 15.在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性指向prototype属性所在函数的指针。例如Person.prototype.constructor指向Person。 16.当调用构造函数创建一个新实例后,该实例的内部将包含一个[[prototype]]指针(主流浏览器中的_proto_属性)**。要明确的重要一点是,这个连接存在于实例与原型对象之间,而不是存在于实例与构造函数之间。** 17.**虽然在所有实现中都无法访问[[prototype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。** ~~~ alert(Person.prototype.isPrototypeOf(person1)); //true alert(Person.prototype.isPrototypeOf(person2)); //true ~~~ 18.**Ojbect.getPrototypeOf()方法返回[[prototype]]的值。** ~~~ alert(Object.getPrototypeOf(person1)) //Person.prototype alert(Object.getPrototypeOf(person1).name) //ken ~~~ 19.虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性,但不会修改那个属性。不过,使用delete操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性。 ~~~ var person1 = new Person(); person1.name = 'jason'; delete person1.name; alert(person1.name); //ken ~~~ 20.单独使用in操作符,会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。 `alert('name' in person1); //true;` 使用for-in循环时,返回的是所有能够通过对象访问的,可枚举的属性,其中既包含存在于实例中的属性,也包括存在原型中的属性。屏蔽了原型中不可枚举属性的实例属性也会在for-in循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的(除了IE8及更早版本)。 要取得对象上所有**可枚举的实例属性**,可以使用ECMAScript5中的***Object.keys()***方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。 如果想要得到所有实例属性,无论它是否可枚举,都可以使用**Object.getOwnPropertyNames()**。 总结: for-in 实例+原型、可枚举; Object.keys() 实例、可枚举; Object.getOwnPropertyNames() 实例、可枚举+不可枚举 21.更简单的原型语法: ~~~ function Person(){ } Person.prototype = { name:'Ken', age:28, job:'FrontEnd Engineer', sayName:function(){ alert(this.name); } }; ~~~ **使用这样的语法创建原型对象,constructor属性不再指向Person了。前面曾经介绍过,每创建一个函数,就会同时创建他的prototype对象。在这里使用的语法,本质上完全重写了默认的prototype对象,因此,constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。此时,尽管instanceof操作符还能返回正确的结果,但通过constructor已经无法确立对象的类型了。** **如果constructor的值真的很重要,那么在使用上面的语法重写原型对象时,可以手动将construtor的属性值设为Person。注意,以这种方式重设constructor属性会导致他的[[Enumerable]]特性被置为true。可以通过Object.defineProperty()方法将属性置为默认的不可枚举:** ~~~ Object.defineProperty(Person.prototype,'constructor',{ enumerable: false, value: Person }); ~~~ 22.原型的动态性 由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来--即使是先创建了实例后修改原型也照样如此。 尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但**如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数与最初原型之间的联系。** 23.不推荐在产品化的程序中修改原生对象的原型。 24.将构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性,这是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。 ~~~ function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.friends = ['wesley','fox']; } Person.prototype = { constructor:Person, sayName:function(){ alert(this.name) } } ~~~ 25.**动态原型模式**解决了构造函数与原型分别独立、没有封装在一起的问题。 ~~~ function Person(name,age,job){ this.name = name; this.age = age; this.job = job; this.friends = ['wesley','fox']; if (typeof this.sayName !== 'function') { Person.prototype.sayName = function(){ alert(this.name) } } } ~~~ 26.**寄生构造函数模式**解决了这样一个场景下的问题:假设想创建一个具有额外方法的特殊数组,又不能直接修改Array构造函数,就可以使用这个模式。**除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的**。 ~~~ function SpecialArray(){ var values = new Array(); values.push.apply(values,arguments); //此处使用apply是因为apply可以方便地把arguments添加到values数组。 values.toPipeString = function(){ return this.join('|'); } return values; } var colors = new SpecialArray('red','blue','green'); alert(colors.toPipeString()); //'red|blue|green' ~~~ 关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与构造函数外部创建的对象没什么不同。为此,**不能依赖instanceof操作符来确定对象类型**。由于存在上述问题,我们建议在在可以使用其他模式的情况下,不要使用这种模式。 ~~~ alert(SpecialArray.prototype.isPrototypeOf(colors)); //false alert(colors instanceof SpecialArray); //false ~~~ 27.所谓**稳妥对象**,指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中(这些环境会禁止使用this和new),或者防止数据被其他应用程序改动时使用。**稳妥构造函数模式遵循与寄生构造函数模式类似的模式,但有两点不同:一是新创建对象实例方法不引用this;二是不使用new操作符调用构造函数。** ~~~ function Person(name,age,job){ //var name = name,age = age,job = job; var o = new Object(); o.sayName = function(){ alert(name); }; return o; } var friend = Person('ken','28','FrontEnd Engineer'); friend.sayName(); //ken ~~~ 这样,变量friend中保存的是一个稳妥对象,而除了调用sayName()方法外,没有别的方式可以访问其数据成员。即使有其他代码会给这个对象添加方法和数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系型,因此instanceof操作符对这种对象也没有意义。 28.ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的。**原型链的基本思想是让一个引用类型继承另一个引用类型属性和方法**。***具体的实现方法就是让一个引用类型的原型等于另一个类型的实例***。 ~~~ function SuperType(){ this.property = true; } SuperType.property.getSuperValue = function(){ return this.property; } function SubType(){ this.subproperty = false; } SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function(){ return this.subproperty; } var instance = new SubType(); alert(instance.getSuperValue) //true ~~~ 要注意,此时instance.constructor现在指向的是SuperType,这是因为SubType的原型指向了另一个对象--SuperType的原型,而这个原型对象的constructor属性指向的是SuperType。 29.由于原型链的关系,我们可以说instance是Object、SuperType、SubType中任何一个类型的实例。通过两种方式来确定原型与实例之间的关系。第一种方式是使用instanceof操作符;第二种方式是使用isPrototypeOf()方法。 `alert(SubType.prototype.isPrototypeOf(instance)); //true;` 30.原型链的主要问题:第一,包含引用类型值的原型属性会被所有实例共享。第二,在创建子类型的实例时,没有办法在不影响那个所有对象实例的情况下,给超类型的构造函数传递参数。 31.**借用构造函数(经典继承)**的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。 ~~~ function SuperType(){ this.colors = ['red','yellow','blue']; } function SubType(){ SuperType.call(this); } ~~~ 通过使用call()或者apply()方法,我们实际上是在(未来将要)创建的SubType实例的环境下调用了SuperType构造函数。这样一来,就会在新SubType()对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就都会具有自己的colors属性的副本。 相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。 ~~~ function SuperType(name){ this.name = name; } function SubType(){ SuperType.call(this,'ken'); this.age = 28; } ~~~ 如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题--方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法(使用prototype定义的),对于子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。 32.**组合继承(伪经典模式)**其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性。 ~~~ function SuperType(name){ this.name = name; this.colors = ['red','blue'.'green]; } SuperType.prototype.sayName = function(){ alert(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(){ alert(this.age); } var instance1 = new SubType('ken',28); instance1.colors.push('black); alert(instance1.colors); //'red,blue,green,black' instance1.sayName(); //'ken' instance1.sayAge(); //28 var instance2 = new SubType('Greg',27); alert(instance2.colors); //'red,blue,green' instance2.sayName(); //'Greg' instance2.sayAge(); //27 ~~~ 组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,成为JavaScript中最常用的继承模式。而且,instanceof和isPrototypeOf()也能够用于识别鲫鱼组合继承创建的对象。 33.**原型式继承**没有严格意义上的使用构造函数,它是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。 ~~~ function object(o){ function F(){} F.prototype = o; return new F(); } ~~~ 在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。例: ~~~ var person = { name: 'Nicholas', friends: ['wesley','fox'] }; var anotherPerson = object(person); anotherPerson.name = 'Greg'; anotherPerson.friends.push('Rob'); var yetAnotherPerson = object(person); yetAnotherPerson.name = 'Linda'; yetAnotherPerson.friends.push('Barbie'); alert(person.friends); //'wesley,fox,Rob,Barbie' ~~~ ES5通过新增Object.creat()方法规范了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。 ~~~ var person = { name: 'Nicholas', friends: ['wesley','fox'] }; var anotherPerson = Object.create(person, { name:{ value: 'Greg' } }); ~~~ 在没有必要兴师动众地创建构造函数,而只是想让一个对象与另个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。 34.**寄生式继承**是与原型式继承密写相关的一种思路,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。 ~~~ function createAnother(original) { var clone = object(original); clone.sayHi = function(){ alert('hi'); }; return clone; } ~~~ 这样创建的新对象不仅具有original的所有属性和方法,而且还有自己的sayHi()方法。在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。 35.**寄生组合式继承** 所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的思路是**:不必为了指定子类型的原型调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。**本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。 ~~~ function inheritPrototype(subType,SuperType){ var prototype = Object(subType.prototype); prototype.constructor = subType; subType.prototype = prototype; } ~~~ ~~~ function SuperType(name){ this.name = name; this.colors = ['red','blue'.'green]; } SuperType.prototype.sayName = function(){ alert(this.name); }; function SubType(name,age) { //继承属性 SuperType.call(this,name); this.age = age; } //继承方法 inheritPrototype(subType,SuperType); SubType.prototype.sayAge = function(){ alert(this.age); } ~~~ **寄生组合式继承与组合继承的区别在于,前者不将超类型中的实例属性继承为子类型的原型属性。**