共计 10832 个字符,预计需要花费 28 分钟才能阅读完成。
1.JavaScript 工厂模式
尽管应用 Object 构造函数或对象字面量能够不便地创建对象,但这些形式也有显著有余:创立具备同样接口的多个对象须要反复编写很多代码。
1.1 什么是工厂模式?
工厂模式是一种家喻户晓的设计模式,广泛应用于软件工程畛域,用于形象创立特定对象的过程。工厂模式是一种创立型模式,简略来说,工厂模式就是创建对象的一种形式。
1.2 工厂模式有什么用?
作用:(1)创建对象;(2)升高代码冗余度。利用场景:当你想要批量生产同品种的对象的时候;比方,你想生成一个班级的 40 个学生,每个学生都有姓名,年龄等特色。这时候你创立一个“工厂”,把信息丢到工厂里,工厂就给你造一个人进去,十分不便
1.3 为什么用工厂模式
从工厂模式的作用登程来看,工厂模式的次要作用就是用来产生对象的。那么别的创建对象的模式有什么毛病?
1.3.1 用字面量的形式创建对象
字面量就是用来形容变量的;一般来说,给变量赋值时,等号左边的都能够看作是字面量(因为等号左边的都是用来形容这个变量的,比方形容一个变量为字符串、数组、对象等)var person = {
name:'zhangsan',
age:18,
gender:'male',
sayName:function(){console.log(this.name);
}
}
毛病:用字面量的形式创建对象,最大的毛病就是,这个对象是一次性的,如果有 40 个同学,这个代码就要写 40 次,有点小麻烦。
1.3.2 new Object()创建对象
Object 是 JavaScript 提供的构造函数;new Object()就是利用 JavaScript 提供的构造函数实例化了一个对象;var person = new Object();
// 为这个实例化的对象增加属性
person.name = 'name';
person.age = 18;
person.gender = 'male';
person.sayName = function(){console.log(this.name);
}
毛病:能够发现它是先实例化了一个对象,而后再为对象增加属性,这样就看不出来是个整体
(像下面的用字面量来创立,属性都包再一个大括号外面,这样子就很好看出这是个整体)
因而,咱们为了使创建对象更加不便(不像字面量创立那样一次性),也为了写的代码更像个整体,就能够交给工厂模式来做。
1.4 应用工厂模式创建对象
// 将创建对象的代码封装在一函数中
function createPerson(name,age,dender){var person = new Object();
person.name = name;
person.age = age;
person.gender = gender;
person.sayName = function(){console.log(this.name);
}
return person;
}
// 利用工厂函数来创建对象
var person1 = createPerson('zhangsan',18,'male');
var person2 = createPerson('lisi',20,'female');
长处:只有咱们往工厂函数外面塞参数,工厂函数就会像生产产品一样造集体进去
毛病:这种形式实质上是将创建对象的过程进行了封装,实质并没有扭转,咱们创立一个 student 时无奈晓得其具体的数据类型,只晓得这是一个对象,往往理论开发中咱们须要确定这个对象到底时个 Person 的实例还是 Dog 的实例。所以,咱们能够应用自定义构造函数模式。
2. 构造函数模式
ECMAScript 中的构造函数是用于创立特定类型对象。像 Object 和 Array 这样的原生构造函数,运行时能够间接在执行环境中应用。当然也能够自定义构造函数,以函数的的模式为本人的对象类型定义属性和办法。JavaScript 中能够自定义构造函数,从而自定义对象类型的属性和办法,构造函数自身也是函数,只不过能够用来创建对象。
2.1 自定义构造函数
后面的案例应用构造函数能够这样写
// 自定义函数
function Person(name,age,gender){
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = function(){console.log(this.name);
}
}
// 利用工厂函数来创建对象
var person1 = new Person('zhangsan',18,'male');
var person2 = new Person('lisi',19,'female');
person1.sayName();//zhangsan
person2.sayName();//lisi
在这个案例中,Person()构造函数代替了 createPerson()工厂函数。实际上,Person()外部的代码跟 createPerson()根本是一样的,只是有如下区别。(1)没有显式地创建对象
(2)属性和办法间接赋值给了 this。(3)没有 return
(4)另外,要留神函数名 Person 的首字母大写了。依照常规,构造函数名称首字母都是要大写的,非构造函数则以小写字母结尾。
2.2 创立 Person 实例
要创立 Person 的实例,应应用 new 操作符。以这种形式调用构造函数会执行如下操作
var person1 = new Person('zhangsan',29,'male');
var person2 = new Person('lisi',18,'female');
(1)在内存中创立一个新对象
(2)这个新对象外部的 [[Prototype]] 个性被赋值为构造函数的 prototype 属性
(3)构造函数外部的 this 被赋值为这个新对象(即 this 指向新对象)
(4)执行构造函数外部的代码(给新对象增加属性)
(5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创立的新对象
person1 和 person2 别离保留这 Person 的不同实例。所有对象都会从它的原型上继承一个 constructor 属性,这两个对象的 constructor 属性指向 Person。
2.3 instanceof
(1)constructor 原本是用于标识对象类型的。不过,个别认为 instanceof 操作符是确定对象类型更牢靠的形式
(2)instanceof 运算符用于检测构造函数的 prototype 属性是否呈现在某个实例对象的原型链上。或者说判断一个对象是某个对象的实例
(3)定义自定义构造函数能够确保实例被标识为特定类型,相比于工厂模式,这是一个很大的益处。在这个案例中,person1 和 person2 之所以也被认为是 Object 的实例,是因为所有自定义对象都继承自 Object。
2.4 应用函数表达式自定义构造函数
构造函数不肯定要写成函数申明的模式。赋值给变量的函数表达式也能够示意构造函数:var Person = function(){
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = function(){console.log(this.name);
};
}
var person1 = new Person('zhangsan',12,'male');
var person2 = new Person('lisi',23,'female');
person1.sayName();//zhangsan
person2.sayName();//lisi
console.log(person1 instanceof Object);//true
console.log(person1 instanceof Person);//true
console.log(person2 instanceof Object);//true
console.log(person2 instanceof Person);//true
补充
在实例化时,如果不想传参数,那么构造函数前面的括号可加可不加。只有有 new 操作符,就能够调用相应的构造函数:function Person(){
this.name = 'larry';
this.sayName = function(){console.log(this.name);
}
}
var person1 = new Person();
var person2 = new Person;
person1.sayName();//larry
person2.sayName();//larry
console.log(person1 instanceof Object);//true
console.log(person1 instanceof Person);//true
console.log(person2 instanceof Object);//true
console.log(person2 instanceof Person);//true
2.5 构造函数也是函数
构造函数与一般函数惟一的区别就是调用形式不同。除此之外,构造函数也是函数。并没有把某个函数定义为构造函数的非凡语法任何函数只有应用 new 操作符调用就是构造函数,而不应用 new 操作符调用的函数就是一般函数。比方,后面的案例中定义的 Person()能够像上面这样调用:var Person = function(name,age,gender){
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = function(){console.log(this.name);
}
}
// 作为构造函数
var person = new Person('jacky',22,'male');
person.sayName();//jacky
// 作为函数调用
Person('lisi',27,'female');// 增加到全局对象 node global 浏览器 window
2.6 构造函数的问题
构造函数尽管也有用,但也并不是没有问题的。构造函数的次要问题在于,其定义的办法会在每个实例上都创立一遍。因而对于后面的案例而言,person1 和 person2 都有名为 sayName()的办法,但这两个办法不是同一个 Function 实例。咱们晓得,ECMAScript 中的函数是对象,因而每次定义函数时,都会初始化一个对象。逻辑上讲,这个构造函数实际上是这样的:function Person(name,age,gender){
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = new Function('console.log(this.name)');// 逻辑等价
}
这样了解这个构造函数就能够更分明地晓得,每个 Person 实例都会有本人的 Function 实例用于显示 name 属性。当然了,以这种形式创立函数会带来不同的作用域链和标识符解析。但创立新 Function 实例的机制是一样的。因而不同实例上的函数尽管同名却不相等。如下所示:console.log(person1.sayName === person2.sayName);//false
因为都是做一样的事,所以没有必要定义两个不同的 Function 实例。况且,this 对象能够把函数与对象的绑定推延到运行时。要解决这个问题,能够把函数定义转移到构造函数内部:function Person(name,age,gender){
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = sayName;
}
function sayName(){console.log(this.name);
}
var person1 = new Person('zhangsan',23,'male');
var person2 = new Person('lisi',22,'female');
person1.sayName();//zhangsan
person2.sayName();//lisi
在这里,sayName()被定义在了构造函数内部。在构造函数外部,sayName 属性等于全局 sayName()函数。因为这一次 sayName 属性中蕴含的只是一个指向内部函数的指针
所以 person1 和 person2 共享了定义在全局作用域上的 sayName()函数。这样尽管解决了雷同逻辑的函数冲定义的问题,但全局作用域也因而被搞乱了,因为那个函数实际上只能只能一个对象上调用。如果这个对象须要多个办法,那么就要在全局作用域中定义多个函数。这会导致自定义类型援用的代码不能很好的汇集在一起。这个新问题能够通过原型模式来解决。
3 原型层级
在通过对象拜访属性时,会依照这个属性的名称开始搜寻。搜素开始对于对象实例自身。如果在这个实例上发现了给定的名称,则返回该名称对应的值。如果没有找到这个属性,则搜寻会沿着指针进入原型对象,而后在原型对象上找到属性后,再返回对应的值。因而,在调用 person1.sayName()时描述产生两步搜寻。1.JavaScript 引擎会问:person1 实例有 sayName()属性吗?答案是没有
2. 持续搜寻并问:person1 的原型有 sayName()属性吗?答案是有。于是就返回了保留在原型上的这个函数。在调用 person2.sayName()时,会产生同样的搜寻过程,而且也会返回雷同的后果。这就是原型用于在多个对象实例间共享属性和办法的原理。看上面的案例:function Person(){
Person.prototype.name = 'zhangsan';
Person.prototype.age = 20;
Person.prototype.gender = 'male';
Person.prototype.sayName = function(){console.log(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.name = 'lisi';
console.log(person1.name);//lisi 来自实例
console.log(person2.name);//zhangsan, 来自原型
在这个案例中,person1 的 name 属性遮蔽了原型对象上的同名属性。尽管 person1.name 和 person2.name 都返回了值,但前者返回的时 'lisi'(来自实例),后者返回的是 "zhangsan"(来自原型)。当 console.log()拜访 person1.name 时,会先在实例上搜寻这个属性。因为这个属性在实例上存在,所以就不会再搜搜原型对象了。而再拜访 person2.name 时,并没有在实例上找到这个属性,所以会持续搜寻原型对象并应用定义在原型上的属性。咱们也能够通过 hasOwnProperty()能够查看拜访的是实例属性函数原型属性。只有给对象实例增加一个属性,这个属性就会遮蔽 (shadow) 原型对象上的同名属性,也就是尽管不会批改它,但会屏蔽对它的拜访。即便在实例上把这个属性设置为 null, 也不会复原它和原型的分割。不过,应用 delete 操作符能够齐全删除实例上的这个属性,从而让标识符解析过程可能持续搜寻原型对象。function Person(){
Person.prototy.name = 'zhangsan';
Person.prototype.age = 20;
Person.prototype.gender = 'male';
Person.prototype.sayName = function(){console.log(this.name)
}
};
var person1 = new Person();
var person2 = new Person();
// 通过 hasOwnProperty()能够查看拜访的是实例属性还是原型属性
console.log(person1.hasOwnProperty('name'));//false
person1.name = 'lisi';
console.log(person1.name)//lisi, 来自实例
// 只在重写 person1 上,name 属性的状况下才会返回 true, 表明此时 name 是一个实例属性, 不是原型属性.
console.log(person1.hasOwnProperty('name'));//true
delete person1.name;
console.log(person1.name);//zhangsan, 来自原型
console.log(person1.hasOwnProperty('name'));//false
这个批改后的案例中应用了 delete 删除 person1.name, 这个属性之前以 'lisi' 遮蔽了原型上的同名属性, 而后原型上 name 属性的分割就复原了, 因而再拜访 person1.name 时, 就会返回原型对象上这个属性的值
3.1 原型与 in 操作符
function Person(){
Person.prototype.name = "zhangsan";
Person.prototype.age = 23;
Person.prototype.gender = 'male';
Person.prototype.sayName = function(){console.log(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
// 无论属性是在实例上还是原型上, 都能够检测到
console.log('name' in person1);//true
console.log('name' in person2);//true
// 判断一个属性是否是原型属性
function hasPrototypeProperty(object,name){
// 不在实例中然而能够拜访到的属性属于原型属性
return !object.hasOwnProperty(name)&&(name in object);
}
console.log(hasPrototypeProperty(person1,'name'));//true
3.2 原生对象的原型
原型模式之所以重要, 不仅体现在自定义类型上, 而且还因为它也是实现所有原生援用类型的模式。所有原生援用类型的构造函数 (包含 Object、Array、String 等) 都在原型上定义了实例办法。比方,数组实例的 sort()办法就是 Array.prototype 上定义的,而字符串包装对象的 substring()办法也是在 String.prototype 上定义的,如下所示:console.log(typeof Array.prototype.sort);//'function'
console.log(typeof String.prototype.substring);//'function'
通过原生对象的原型能够获得所有默认办法的援用,也能够给原生类型的实例定义新的办法。能够像批改自定义对象原型一样批改原生对象原型,因而随时能够增加办法。比方,上面的代码就给 String 原始值包装类型的
实例增加了一个 last()办法:// 给字符串增加属性或办法 要写到对应的包装对象的原型下才行
var str = 'hello';
String.prototype.last = function () {
// 返回指定地位的字符
return this.charAt(this.length - 1);
};
console.log(str.last()); // o
如果给定字符串调用 last()办法,那么该办法会返回 给定字符串的最初一个字符。因为这个办法是被定义在 String.prototype 上,所以以后环境下所有的字符串都能够应用这个办法。str 是个字符串,在读取它的属性时,后盾会主动创立 String 的包装实例,从而找到并调用 last()办法。留神:只管能够这么做,但并不举荐在产品环境中批改原生对象原型。这样做很可能造成误会,而且可能引发命名抵触。另外还有可能意外重写原生的办法。
3.3 更简略的原型模式
在后面的案例中,每次定义一个属性或办法都会把 Person.prototype 重写一遍。为了缩小代码冗余,也为了从视觉上更好地封装原型性能,间接通过一个蕴含所有属性和办法的对象字面量来重写原型成为了一种常见的做法,如上面的案例所示:function Person() {}
Person.prototype = {
name: "zhangsan",
age: 29,
gender: "male",
sayName() {console.log(this.name);
}
};
// 在这个案例中,Person.prototype 被设置为等于一个通过对象字面量创立的新对象。最终后果是一样的,只有一个问题:这样重写之后,Person.prototype 的 constructor 属性就不指向 Person 了。在创立函数时,也会创立它的 prototype 对象,同时会主动给这个原型的 constructor 属性赋值。而下面的写法齐全重写了默认的 prototype 对象,因而其 constructor 属性也指向了齐全不同的新对象(Object 构造函数),不再指向原来的构造函数。var person1 = new Person()
console.log(person1.constructor === Person); //false
console.log(person1.constructor === Object); //true
那怎么解决这个问题呢?
能够在重写原型对象时,专门设置 constructor 的值;然而,以这种形式复原 constructor 属性会创立一个 [[Enumerable]] 为 true 的属性。而原生 constructor 属性默认是不可枚举的。因而,如果你应用的是兼容 ECMAScript 的 JavaScript 引擎,那可能会改为应用 Object.defineProperty()办法来定义 constructor 属性:function Person() {}
Person.prototype = {// 这种形式复原 constructor 属性会创立一个 [[Enumerable]] 为 true 的属性
//constructor: Person,
name: "zhangsan",
age: 29,
gender: "male",
sayName() {console.log(this.name);
}
};
// 复原 constructor 属性
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
var person1 = new Person()
console.log(person1.constructor == Person); //true
console.log(person1.constructor == Object); //false
3.4 原型的问题
(1)原型模式也不是没有问题。首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都获得雷同的属性值。尽管这会带来不便,但还不是原型的最大问题。原型的最次要问题源自它的共享个性。(2)咱们晓得,原型上的所有属性是在实例间共享的,这对函数来说比拟适合。另外蕴含原始值的属性也还好,如后面案例中所示,能够通过在实例上增加同名属性来简略地遮蔽原型上的属性。真正的问题来自蕴含援用值的属性。来看上面的案例:function Person() {}
Person.prototype = {
constructor: Person,
name: "zhangsan",
friends: ["lisi", "wangwu"],
sayName() {console.log(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("zhaoliu");
console.log(person1.friends); // ['lisi', 'wangwu', 'zhaoliu']
console.log(person2.friends); // ['lisi', 'wangwu', 'zhaoliu']
console.log(person1.friends === person2.friends); // true
这里,Person.prototype 有一个名为 friends 的属性,它蕴含一个字符串数组。而后这里创立了两个 Person 的实例。person1.friends 通过 push 办法向数组中增加了一个字符串。因为这个 friends 属性存在于 Person.prototype 而非 person1 上,新加的这个字符串也会在(指向同一个数组的)person2.friends 上反映进去。如果这是无意在多个实例间共享数组,那没什么问题。但一般来说,不同的实例应该有属于本人的属性正本。这就是理论开发中通常不独自应用原型模式的起因。
4. 组合模式
组合应用构造函数模式和原型模式。构造函数用于定义实例属性,原型模式用于定义方法和共享属性。这种模式是目前在 ECMAScript 中应用最宽泛,认同度最高的一种创立自定义类型的办法
function Person(name, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
this.firends = ['zhangsan', 'lisi'];
}
Person.prototype = {
constructor: Person,
sayName: function () {console.log(this.name);
}
};
var p1 = new Person('larry', 44, 'male');
var p2 = new Person('terry', 39, 'male');
p1.firends.push('robin');
console.log(p1.firends); // ['zhangsan', 'lisi', 'robin']
console.log(p2.firends); // ['zhangsan', 'lisi']
console.log(p1.firends === p2.firends); // false
console.log(p1.sayName === p2.sayName); // true
正文完
发表至: javascript
2021-09-07