-1). 构造函数 :
如果接触过后端语言的朋友相信对 构造函数
是异常的敏感吧,其实在 Java
中创建对象的常用方式有两种,分别是 构造函数
、反射
。其中 构造函数
是最常用的。但是请注意 JavaScript
中的构造函数与 Java
中的构造函数是不一样的,切不可将二者等同视之。并且针对于原型来说也是 Java
所不曾拥有的特殊机制。但是 JavaScript 中的构造函数可以用来创建对象这一功能却是与 Java 中的构造函数是相同的。
function Person(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
this.sayName = function() {
console.log(this.name);
};
}
// 创建实例
var person_1 = new Person('LJ', 22, '敲代码');
var person_2 = new Person('XXY', 23, '会计师');
console.log(person_1.constructor);// Person
console.log(person_2.constructor);// Person
console.log(person_1 instanceof Person);// true
console.log(person_2 instanceof Person);// true
console.log(person_1.sayName === person_2.sayName);// false
上面示例就是一个构造函数,通过构造函数创建实例的方式就是通过使用 new
操作符来操作。
=>
对于 "构造函数" 而言不要纠结于这个写法。因为在 JavaScript
中不是那样写的才是构造函数,事实上任何一个函数都有可能是构造函数,任何一个构造函数都可以当普通函数来用,即使是普通函数也是可以通过 new
的方式创建对象的,但是出于严谨、效率等综合方面考虑,建议构造函数名的首字母要大写与普通函数区别开来。
// 构造函数
function Person(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
this.sayName = function () {
print(this.name);
};
print(`我是一名: ${this.hobby}`);
}
// 创建实例 :
var person_1 = new Person('LJ', 22, '程序员');
person_1.sayName();// LJ
// 普通函数当做构造函数
function person() {
// ...
}
var person_2 = new person();
console.log(person_2);// person {}
// 构造函数当做普通函数
Person('LJ', 22, '敲代码');// 我是一名: 敲代码
// 在另一个作用域使用
var o = {
name: 'XXY',
age: 23,
hobby: '会计师'
};
Person.call(o, o.name, o.age, o.hobby);// 我是一名: 会计师
-2). 原型 :
(1). 每一个函数建立之初都有一个 prototype
属性指向它的原型对象,并且由这个函数创建的实例也有一个 [[ prototype ]]
属性指向其构造函数的原型对象。
(2). 在原型对象创建的属性与方法都是被每个实例所共享的。
(3). 实例访问其原型对象的方式有两种一种是通过 _proto_
属性,另一种则是 Object.getPrototypeOf(obj)
方法。
(4). 判断一个对象是否是另一个对象的原型对象可使用 isPrototypeOf(obj)
方法,是则返回 true
,否则返回 false
(5). 判断一个属性是否属于实例本身的属性使用 hasOwnProperty(property)
,是则返回 true
,不是则返回 false
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function() {
console.log(this.name);
};
var person_1 = new Person('LJ', 22);
var person_2 = new Person('XXY', 23);
console.log(Person.prototype);// {sayName: ƒ, constructor: ƒ}
console.log(Person.prototype === person_1.__proto__);// true
console.log(Person.prototype === Object.getPrototypeOf(person_2));// true
console.log(person_1.sayName === person_2.sayName);// true
console.log(Person.prototype.isPrototypeOf(person_1));// true
console.log(person_1.constructor);// Person
console.log(Object.getPrototypeOf(person_2).constructor);// Person
(6). 可能这么说你不会感觉原型究竟有什么用,可以做些什么 ? 举个简单的例子 , 我们平时用的数组不就是 Array
类型的一个实例嘛,笔者记着 第 12 更 的时候清楚明确的介绍了 JavaScript
中的关于数组的相关内容里面有着很多关于数组的操作方法,难道这些方法都是定义在我们创建的实例身上的吗 ? 首先笔者问道,那些方法是你给定义上去的吗 ? 很明显不是,那为什么我们还可以访问的得到这些方法呢 ? 原因就是 ECMAScript
将这些方法定义在了 Array
类型的原型对象上,所以他的实例本身没有这些方法但却是可以访问原型对象中定义的方法,这就是明明没有亲自在某个引用类型的实例上面定义属性和方法我们却依然可以使用它们的原因。说到这其实仔细观察一下上面我们展示的示例代码中对象对 sayName()
方法的访问情况就可见一斑了。
(7). 但是在原型上定义属性和方法就一定实最佳选择吗 ? 其实不是的,我们都知道定义在原型上面属性与方法都被每个其构造函数的实例所共享,所以就面临着一些问题,比如 A
实例给 name
属性改了一个值等到 B
实例再去访问的时候也会是改过后的值,所以如果实例之间本身准备要共享某属性或方法那还好说,万一需要实例之间的属性与方法的结果有所不同呢 ? 所以原型需要慎用,用好了是一把利器用不好可能还不如不用。但是有一个特性可以解决上面这个问题,就是如果实例需要不同的属性值或方法可以直接在当前实例本身定义一个同名的属性或者方法就会将其指向的原型对象上定义的同名的属性与方法覆盖掉,从而实现个性。
function Person(name) {
this.name = name;
}
Person.prototype.name = 'LJ';
var person = new Person('XXY');
console.log(person.name);// XXY
delete person.name;
console.log(person.name);// LJ
这样一来第一次访问 name
属性实际上访问的是 person
实例上的 name
属性,因为实例上的属性如果与其指向的原型对象上的属性重名的话,以当前实例上面的属性为准,所以会打印 XXY
,但是我们通过 delete
操作符移除了 person
实例上的 name
属性,然后再去访问 person
实例的 name
属性就只能去其指向的原型对象上去找了。
(8). in
操作符可以判断某个属性是否存在,无论是在对象身上可以访问的到亦或是原型身上可以访问的到。
function Person(name) {
this.name = name;
}
Person.prototype.age = 22;
var person = new Person('LJ');
console.log('name' in person);
console.log('age' in person);
(9). 自定义判断原型属性的方法 :
function hasOwnPrototype(property) {
return !this.hasOwnProperty(property) && (property in this);
}
function Fun(name){
this.name = name;
}
Fun.prototype.age = 22;
Fun.prototype.hasOwnPrototype = hasOwnPrototype;
var obj = new Fun('LJ');
console.log(obj.hasOwnPrototype('name'));// false
console.log(obj.hasOwnPrototype('age'));// true
(10). Object.keys( ... )
获取对象身上所有的可枚举的属性
function Person(name) {
this.name = name;
}
Person.prototype.age = 22;
console.log(Object.keys(Person.prototype));// ["age"]
因为我们使用的是原型对象做 Object.keys( ... )
的参数所以检测的也只是 Person
的原型对象而不是 Person
类型的实例。所以仅仅获取了 age
属性,还有一个 constructor
属性是不可被枚举的所以 Object.keys( ... )
没有获取到。
(11). Object.getOwnPropertyNames( ... )
方法可以获取一个对象的所有可枚举和不可枚举的属性
function Person(name) {
this.name = name;
}
Person.prototype.age = 22;
console.log(Object.getOwnPropertyNames(Person.prototype));
(12). 更简单的原型语法
: 平时我们在原型上面定义属性与方法都是 Xxx.prototype.Xxx
的方式。为了减少不必要的输入可以简化一下形式 :
这样设置后会导致两个问题 :
A. Person
构造函数的原型指针不再指向原生的 Person
原型对象,而是指向了一个 Object 类型的实例,所以其 constructor
势必会指向 Object
而不是 Person
。
function Person() {
Person.prototype = {
name: 'LJ',
age: 22,
job: '程序员',
sayName() {
console.log(this.name);
}
};
}
var person = new Person();
console.log(Person.prototype);// {name: "LJ", age: 22, job: "程序员", sayName: ƒ}
console.log(Person.prototype.constructor);// ƒ Object() { [native code] }
所以原型中定义属性时候就一并将 constructor
属性指定好 :
function Person() {
Person.prototype = {
constructor: Person,
name: 'LJ',
age: 22,
job: '程序员',
sayName() {
console.log(this.name);
}
};
}
var person = new Person();
console.log(Person.prototype);// {constructor: ƒ, name: "LJ", age: 22, job: "程序员", sayName: ƒ}
console.log(Person.prototype.constructor);// Person
但是又有一个问题就是 constructor
属性是不可被枚举的,在上面我们也提到过,但是经这么一定义就变成可枚举的了 :
function Person() {
Person.prototype = {
constructor: Person,
name: 'LJ',
age: 22,
job: '程序员',
sayName() {
console.log(this.name);
}
};
}
var person = new Person();
console.log(Object.keys(Person.prototype));
所以利用前面学到的知识将其变换不可被枚举 :
function Person() {
Person.prototype = {
constructor: Person,
name: 'LJ',
age: 22,
job: '程序员',
sayName() {
console.log(this.name);
}
};
}
var person = new Person();
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false
});
console.log(Object.keys(Person.prototype));
这样一来 constructor
属性既指向了 Person
构造函数,又实现了 constructor
属性的不可被枚举。
B. 虽然构造函数的原型指针已经不指向原生的构造函数了但是其实例的原型指针依然指向的还是原生的原型对象。
function Person() {
Person.prototype = {
constructor: Person,
name: 'LJ',
age: 22,
job: '程序员',
sayName() {
console.log(this.name);
}
};
}
var person = new Person();
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false
});
console.log(person.proto__ === Person.prototype);// false
console.log(person.name);// undefined
-1). 为什么高效的创建对象呢 ?
两点原因 :
(1). 批量创建对象的时候很明显一个个的使用字面量来创建的话,效率就太低了。
(2). 另外就是平常我们使用字面量的方式来创建对象的时候,定义的属性与方法每个对象都有自己的一份,一些可以共享的方法我们应该将其抽取出来而不是每个对象身上都有一套,浪费内存空间。
-2). 怎样高效的创建对象 ?
想要高效的创建对象必须要借助于一定的模式。在 JavaScript
中类似这样高效的模式共有7
种。分别是 :
=>
自己把 console.log()
函数仍在了一个函数里面,因为需要写太长所有想简便一下 :
// 封装打印函数
function print(arg) {
console.log(arg);
}
(1). 工厂模式(factory mode) :
function person(name, age, job) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.job = job;
obj.sayName = function() {
print(`name: ${obj.name}`);
};
return obj;
}
// 创建实例
var person_1 = person('LJ', 22, '程序员');
var person_2 = person('XXY', 23, '会计师');
print(person_1);// { name: 'LJ', age: 22, job: '程序员', sayName: [Function] }
print(person_2);// { name: 'XXY', age: 23, job: '会计师', sayName: [Function] }
print(person_1.sayName === person_2.sayName);// false
print(person_1.constructor);// [Function: Object]
print(person_2.constructor);// [Function: Object]
print(Object.getPrototypeOf(person_1).constructor);// [Function: Object]
由上述示例我们可知道这个所谓的工厂模式啊无非就是封装了我们平常创建对象的细节,等我们使用工厂模式来创建对象的时候可以直接调用方法传入参数即可。
优点
: 这种模式的优点就是批量创建对象极为方便,可以实现代码复用,不用创建对象的时候书写重复定义属性与方法的代码。
缺点
: 这种模式的缺点 :
A. 创建的实例的类型不确定,上述的示例代码我们看到两个实例所指向原型对象的 constructor
属性是指向的 Object
类型的,而不是具体某个类型。
B. 所创建的每个实例身上都有一套自己的属性与方法,有些属性与方法是不需要有个性的,言外之意有些属性与方法是可以共享的,但是使用工厂模式创建出来的实例身上都有着自己的一套,这会浪费内存空间。
(2). 构造函数模式(constructor mode) :
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
print(`name: ${this.name}`);
}
}
// 创建实例
var person_1 = new Person('LJ', 22, '程序员');
var person_2 = new Person('XXY', 23, '会计师');
print(person_1);// Person { name: 'LJ', age: 22, job: '程序员', sayName: [Function] }
print(person_2);// Person { name: 'XXY', age: 23, job: '会计师', sayName: [Function] }
print(person_1.sayName === person_2.sayName);// false
print(person_1.constructor);// [Function: Person]
print(person_2.constructor);// [Function: Person]
print(Object.getPrototypeOf(person_1).constructor);// [Function: Person]
这段代码就是构造函数模式,跟我们刚开始介绍的构造函数的代码出不多是一样的。该模式优点
: 这种模式可以进行批量创建对象并且可以明确所创建的每个实例的类型,可以批量创建对象。
缺点
: 这种模式的缺点就在于没有改变每个实例身上都各有自己的一套属性与方法这个弊端,(只有个性缺乏共性),占用不必要的内存空间。
=>
修改 : 将准备共享的属性或者函数定义成当前构造函数所在作用域中的属性或者方法然后在构造函数内部引用。
// 改进
function People(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
}
// 创建实例
var people_1 = new People('LJ', 22, '敲代码');
var people_2 = new People('XXY', 23, '会计师');
people_1.sayName();// LJ
people_2.sayName();// XXY
print(people_1.sayName === people_2.sayName);// true
我们可以看到经过改进以后确实达到了我们的需求但是却违背了封装性的基本要求,本来我们是要封装创建对象的细节的这样一来就会有很多的属性或者方法暴露在外边,与我们的要求不符,所以这种改进的方式也是不被提倡的。
(3). 原型模式(prototype mode) :
function Person() {
Person.prototype.name = 'LJ';
Person.prototype.age = 22;
Person.prototype.hobby = '敲代码';
Person.prototype.sayName = function() {
print(this.name);
};
}
// 创建实例
var person_1 = new Person();
var person_2 = new Person();
print(person_1.name === person_2.name);// true
print(person_1.name);// LJ
print(person_2.name);// LJ
print(person_1.sayName === person_2.sayName);// true
person_1.__proto__.name = 'XXY';
print(person_1.name);// XXY
print(person_2.name);// XXY
优点
: 原型模式的优点就是可以进行批量创建对象,可以实现相同构造函数创建的对象之间方法的共享,减少了不必要的内存浪费,所创建的实例的所属类型明确。
缺点
: 原型模式的缺点在于,所有的属性与方法都是共享的就意味着 A
实例修改了原型中的某个属性值或者方法会反映到 B
实例上,当 B
实例访问的时候其实已经是 A
修改后的,即缺乏个性。
(4). 组合构造函数与原型模式(combination constructor and prototype model)
鉴于以上的方式都未达到我们理想的效果,构造函数模式的实例之间具有个性很多相同功能的属性或者方法都被单独定义着,浪费内存空间。原型模式的实例之间具有共性所有的属性与方法都是共享的,任何一个实例更改了其所指向的原型中的属性或方法后,其他的实例再去访问,访问到的都是已经被更改了的。
正是因为以上两种模式均未成为一个完美的对象创建模式,所以将二者组合起来一定可以达到需求了。
function Person(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
}
Person.prototype.sayName = function() {
consoel.log(this.name);
};
// 创建实例
var person_1 = new Person('LJ', 22, '敲代码');
var person_2 = new Person('XXY', 23, '会计师');
print(person_1.sayName === person_2.sayName);// true
print(person_1.name === person_2.name);
print(person_1.constructor);// [Function: Person]
print(person_2.constructor);// [Function: Person]
print(Object.getPrototypeOf(person_1).constructor);// [Function: Person]
上述模式的代码就将构造函数与原型巧妙的结合在了一起,使得规避了构造函数的缺点、原型的缺点。是一种比较可行的创建对象的模式。
优点
: 组合构造函数与原型的优点就是将两种模式之长充分发挥出来,使得通过该模式创建的实例既有个性的属性与方法,也有共享的属性与方法,这样一来既不必浪费不必要的内存空间了。并且也可以实现批量的创建对象,并且所创建的实例所属的类型也十分明确。
缺点
: 虽然规避了两个重要的问题,但是其写法将构造函数与原型分离,这无疑打破了封装性的规则,看着比较怪异。
(5). 动态原型模式(dynamic prototype mode)
function Person(name, age, hobby) {
this.name = name;
this.age = age;
this.hobby = hobby;
if(typeof this.sayName !== 'function') {
Object.getPrototypeOf(this).sayName = function() {
print(this.name);
}
}
}
// 创建实例
var person_1 = new Person('LJ', 22, '敲代码');
var person_2 = new Person('XXY', 23, '会计师');
print(person_1.sayName === person_2.sayName);// true
print(person_1.name === person_2.name);// false
print(person_1.constructor);// [Function: Person]
print(person_2.constructor);// [Function: Person]
print(Object.getPrototypeOf(person_1).constructor);// [Function: Person]
print(Object.getPrototypeOf(person_2).constructor);// [Function: Person]
动态原型模式时候到底也是将构造函数与原型结合在了一起,与组合构造函数与原型模式相比本质上并无太多变化,唯一变化的大概就是将原型的细节给封装了起来,结束了组合构造函数与原型模式中构造函数与原型写法上的分离状态。
优点
: 该模式的优点是可以实现对象的批量创建,也解决了构造函数与原型面临的弊端,还结束了组合构造函数与原型模式的区别。更重要的是在构造函数内部用了一个小判断来提升性能这就避免了每次创建实例的时候都在原型上重新定义一次共享的属性或者方法。
缺点
: 无,是一个比较可行也是应用比较广泛的一种高效的创建对象的模式。
(6). 寄生构造函数模式(parasitic constructor mode)
function Person(name, age, hobby) {
var o = new Object();
o.name = name;
o.age = age;
o.hobby = hobby;
o.sayName = function() {
print(this.name);
};
return o;
}
// 创建实例
var person_1 = new Person('LJ', 22, '敲代码');
var person_2 = new Person('XXY', 22, '算数');
print(person_1.name);// LJ
print(person_2.name);// XXY
print(person_1.sayName === person_2.sayName);// false
print(person_1.constructor);// [Function: Object]
print(Object.getPrototypeOf(person_1).constructor);// [Function: Object]
优点
: 该模式可以实现对象的批量创建
缺点
: 该模式创建的每个实例都自己独有一份构造函数中定义的属性与方法,只有个性没有共性。有些属性与方法没有必要在每个实例上都重新定义一次,应该将他们共享,否则浪费内存空间,无法确定该模式下创建的实例的所属类型。
(7). 稳妥/安全 构造函数模式(safe constructor mode)
function Person(name, age, hobby) {
var o = new Object();
var _name = name;
var _age = age;
var _hobby = hobby;
o.sayName = function() {
print(_name)
};
return o;
}
var person_1 = Person('LJ', 22, '敲代码');
var person_2 = Person('XXY', 23, '算数');
person_1.sayName();// LJ
print(person_1.name);// undefined
print(person_2.name);// undefined
print(person_1.constructor);// [Function: Object]
print(Object.getPrototypeOf(person_1).constructor);// [Function: Object]
该模式规定在构造函数内部不引用 this
在函数外部不使用 new
操作符来创建对象,这无疑是出于安全的角度考虑 。
优点
: 该模式可以实现对象的批量创建
缺点
: 该模式创建的每个实例都自己独有一份构造函数中定义的属性与方法,只有个性没有共性。有些属性与方法没有必要在每个实例上都重新定义一次,应该将他们共享,否则浪费内存空间,无法确定该模式下创建的实例的所属类型。
-3). 工厂模式、寄生构造函数模式、稳妥构造函数模式 三者的异同 :
同 : 创建的每个实例都自己独有一份构造函数中定义的属性与方法, 浪费内存空间,无法确定该模式下创建的实例的所属类型,可以实现对象的批量创建
异 : 工厂模式与寄生构造函数模式本质上没有区别,唯一一个区别就是创建对象的方式前者直接接收函数返回的对象,而后者则是通过 new
操作符来创建实例,本来通过 new
操作符创建的实例是当前构造函数的实例但是如果在函数内部return
了另一个对象,那么外部在 new
的时候得到的实例就是当前构造函数中的返回值而不是当前构造函数的一个实例。
// 工厂模式
function Foo() {return new Object();}
var obj = Foo();
console.log(obj.constructor);// ƒ Object() { [native code] }
console.log(Object.getPrototypeOf(obj));// {constructor: ƒ, __defineGetter__: ƒ, __lookupGetter__: ƒ, …}
// j寄生构造函数模式
function Fun() {return new Object();}
var obj2 = new Fun();
console.log(obj2.constructor);// ƒ Object() { [native code] }
console.log(Object.getPrototypeOf(obj2));// {constructor: ƒ, __defineGetter__: ƒ, __lookupGetter__: ƒ, …}
console.log(obj.__proto__ === Foo.prototype);// false
console.log(Object.getPrototypeOf(obj2) === Fun.prototype);// false
稳妥构造函数模式与工厂模式和寄生构造函数模式的区别就是,稳妥构造函数模式中构造函数内部不使用 this
,在外部不使用 new
关键字来创建对象并且外部创建好的实例只能访问到构造函数通过公共 "接口"(公共方法) 暴露出来的值而不能去改动它,因为其被在构造函数内部被定义成局部变量而不是被添加到返回对象上。
网站声明:如果转载,请联系本站管理员。否则一切后果自行承担。
加入交流群
请使用微信扫一扫!