知识点1:属性与特性
ECMAScript中没有类的概念,可把对象想象成一组名值对,其中值可以是数据或函数(属性和方法)。每个对象基于一个引用类型创建。例:
var person = {
name: "Bob",
age: 29,
sayName: function(){
alert(this.name);
}
};
属性在创建时都带有一些特性,这些特性是为实现引擎使用的,在JavaScript中不能直接访问。属性又可分为数据属性和访问器属性,其中数据属性有四个描述行为的特性:
1.[[Configurable]]:能否通过delete删除属性,能否修改属性特性,或能否修改为访问器属性。像上例那样直接在对象上定义的属性默认为true;
2.[[Enumerable]]:能否通过for-in循环返回属性。像上例那样直接在对象上定义的属性默认为true;
3.[[Writable]]:能否修改属性的值。像上例那样直接在对象上定义的属性默认为true;
4.[[Value]]:包含这个属性的数据值。读取属性值时从这个位置读,写时把新值保存在这里。默认为undifined。
要修改属性默认的特性,必须使用Object.defineProperty(),该方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。例:
Object.defineProperty(person, "name", {
writable: false,
value: "Alis"
});
而访问器属性不包含数据值,有一对getter和setter函数。读取访问器属性时会调用getter(),写入则调用setter()。访问器属性也有四个特性:
1.2.[[Configurable]]、[[Enumerable]]与数据属性类似;
3.4[[Get]]、[[Set]]:读取或写入属性时调用,两者不一定非要同时指定。
访问器属性不能直接定义,必须使用Object.defineProperty()。
另外,可以用Object.defineProperties()一次定义多个属性;读取属性特性则通过Object.getOwnPropertyDescriptor(),参数是属性所在对象和属性名称,返回值为一个描述符对象。
知识点2:工厂模式与构造函数模式
工厂模式是软件工程领域常用设计模式,抽象了创建具体对象的过程,在JavaScript中实现如:
function createPerson(name, age){
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function(){ alert(this.name); };
return o;
}
var person1 = createPerson("Bob", 27);
工厂模式虽然解决了创建多个相似对象的问题,但无法去识别一个对象的类型。因此出现构造函数模式如:
function Person(name, age){
this.name = name;
this.age = age;
this.sayName = function(){ alert(this.name); };
}
var person2 = new Person("Bob", 27);
按照惯例构造函数始终以一个大写字母开头。构造函数与其他函数唯一区别就是调用方式不同,任何函数只要通过new来调用都可作为构造函数。使用new操作符调用构造函数会经历4个步骤:创建新对象,将构造函数作用域赋给新对象(this指向新生成的对象实例),执行构造函数中的代码,返回新对象。通过instanceof操作符可验证对象类型。
使用构造函数的主要问题是每个方法都要在每个实例上重新创建一遍,不同实例上的同名函数是不相等的。
知识点3:原型模式
每个函数都有一个prototype(原型)属性,该属性是一个指针指向一个对象(原型对象),而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。因此,可以将属性方法直接添加到原型对象中,如:
function Person(){
}
Person.prototype.name = "Bob";
Person.prototype.age = 27;
Person.prototype.sayName = function(){ alert(this.name); };
var person3 = new Person();
当调用构造函数创建一个新实例后,该实例内部将包含一个指针指向构造函数的原型对象。每当代码读取对象的某个属性时,会先搜索实例本身,若没有找到则搜索指针指向的原型对象,在原型对象中找到则将其返回。因此实例中的属性会屏蔽原型中的同名属性,除非通过delete操作符删除实例属性。
默认情况下所有原型对象会自动获得一个constructor属性,该属性是一个指向prototype属性所在函数的指针。上例中Person.prototype.constructor指向Person。原型最初只包含constructor属性。
Object.getPrototypeOf()可以获取一个对象的原型;Object.keys()接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组,不包括原型属性。
对象实例的isPrototypeOf()用来判断一个对象是否是另一个对象的原型;对象实例的hasOwnProperty()可以检测一个属性是否存在于实例还是原型中,存在于实例中才返回true。
in操作符会在通过对象能访问给定属性时返回true,无论存在于实例还是原型中。
原型模式的缺点一是省略了为构造函数传递初始化参数的环节;二是由于共享的本性,对于包含引用类型值的属性来说,修改一个实例会影响全部实例。因此很少单独使用原型模式。
知识点4:组合使用构造函数模式和原型模式
目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法是组合使用两种模式:构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。如:
function Person(name, age){
this.name = name;
this.age = age;
this.friends = ["Bob", "Court"];
}
Person.prototype = {
constructor : Person,
sayName : function(){ alert(this.name); }
}
注意上面这种对象字面量的写法本质上重写了默认原型对象,因此constructor属性不再指向Person,需显式设置。另外如果在已经创建了实例的情况下使用对象字面量重写原型,会切断现有实例与新原型之间的联系。
知识点5:动态原型模式
有人可能会对独立的构造函数和原型感到困惑,其实可以将所有信息都封装在构造函数中:
function Person(name, age){
this.name = name;
this.age = age;
if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){ alert(this.name); }
}
}
定义sayName方法的代码只会在初次调用构造函数时才会执行。而且if语句检查的可以是初始化后应该存在的任何属性或方法,不必用一堆if语句检查每个属性和方法。
知识点6:原型链与继承
JavaScript通过原型链来实现继承。考虑到每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,实例都包含一个指向原型对象的内部指针。因此如果重写原型对象,代之以一个新类型的实例,那此时原型对象将包含一个指向另一个原型的内部指针,假如另一个原型又是另一个类型的实例,那么层层递进就构成了实例与原型的链条(原型链),使一个对象拥有定义在其他对象中的属性和方法。写法如:SubType.prototype = new SuperType();
所有引用类型默认继承了Object就是通过原型链实现的,因为所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针指向Object.prototype;而instanceof操作符用于测试实例是否为某个构造函数的实例,其原理是也是检查原型链,对于实例与原型链中出现过的构造函数结果都会返回true。
Object.getOwnPropertyNames()返回一个数组,成员是对象本身的所有属性的键名,不包含继承的属性键名。
使用对象字面量添加原型方法时会重写原型链。
原型链存在两个问题。一是在通过原型实现继承时,原型实际上会变成另一个类型的实例,于是原先的实例属性变成了现在的原型属性,导致引用类型属性的共享问题;二是没有办法在不影响所有对象实例的情况下向超类型的构造函数传递参数。因此,实践中很少单独使用原型链。
知识点7:借用构造函数与组合继承
解决原型中包含引用类型值的问题可使用一种叫做借用构造函数的技术,使每个实例都有自己的属性。如:
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
function SubType(){
SuperType.call(this, "Nicholas");
this.age = 29;
}
var instance1 = new SubType();
即在新创建的子类型实例的环境下调用超类型的构造函数,执行超类型构造函数中定义的所有对象初始化代码。如此还可以向超类型构造函数传递参数。
借用构造函数依然存在问题,比如函数无法复用。因此将原型链和借用构造函数组合一起称为组合继承,思路是利用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。如:
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
Super.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("Bob", 20);
重点为三步:子类型构造函数中调用父类型构造函数、重设子类型原型、重设子类型原型的constructor。组合继承是JavaScript中最常用的继承模式。
知识点8:寄生组合式继承
组合继承最大的问题就是会调用两次超类型构造函数,一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。后者会在创建实例属性时屏蔽原型中的同名属性。
而寄生组合式继承的思路是不必为了指定子类型的原型而调用超类型的构造函数,所需要的无非就是超类型原型的一个副本而已。因此可使用Object.create创造一个对象,它将参数作为新创建对象的原型对象。
subType.prototype = Object.create(superType.prototype);
subType.prototype.constructor = subType;
该函数实现了寄生组合式继承最简单形式,接收两个参数分别是子类型构造函数和超类型构造函数。用其替换前述为子类型原型赋值的语句。
开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
知识点9:call、apply和bind
三者都用来改变函数执行时的上下文。call()使用一个指定的this值和单独给出的一个或多个参数来调用函数;
apply()与call()的唯一区别在于apply接受的是一个参数数组;bind()返回一个原函数的拷贝供以后调用,并拥有指定的this值和初始参数。例:
// 有只猫叫小黑,小黑会吃鱼
var cat = {
name: '小黑',
eatFish(...args) {
console.log(this.name + '吃鱼');
},
}
// 有只狗叫大毛,大毛会吃骨头
var dog = {
name: '大毛',
eatBone(...args) {
console.log(this.name + '吃骨头');
},
}
// 有一天大毛想吃鱼了,可是它不知道怎么吃。小黑说我吃的时候喂你吃
cat.eatFish.call(dog, '汪汪汪', 'call')
// 大毛为了表示感谢,决定下次吃骨头的时候也喂小黑吃
dog.eatBone.call(cat, '喵喵喵', 'call')
// 也可使用apply
cat.eatFish.apply(dog, ['汪汪汪', 'apply'])
dog.eatBone.apply(cat, ['喵喵喵', 'apply'])
// 有一天他们觉得每次吃的时候喂太麻烦了,干脆直接教对方怎么吃
cat.eatFish.bind(dog, '汪汪汪', 'bind')();
dog.eatBone.bind(cat, '喵喵喵', 'bind')();