3242 字
16 分钟
创建对象之原型模式

原型模式#

每一个函数都会创建一个prototype的属性

原型方式

function Person(){}
Person.prototype.name = 'weng';
Person.prototype.age = 23;
Person.sayName = function(){
console.log(this.name);
}
let per1 = new Person();
ler per2 = new Person();
per1.sayName(); // weng

1.与构造函数的不同#

原型模式是直接再构造函数的prototype属性上加对象和相应的属性(也就是再构造函数的原型对象上面加属性)

与构造函数不同的是,使用这种原型模式定义的属性和方法,之后创建出来的实例,都是一起共享这些属性的,因此

上述的per1和per2的sayName()函数指向的是同一个指针, 他们的原型也同样都是Person构造函数指向的原型对象

console.log(per1.sayName = per2.sayName);

2. 理解原型#

自定义构造函数的时候,原型对象,也就是构造函数的原型对象(prototype指向的对象),会自动获得一个constructor的属性,这个属性是重新指向自定义的构造函数,如果没有父类的话,其他的所有方法都是继承于js内部的Object对象的所有方法。

console.log(Person.prototype.constructor === Person; // true;

例如,根据上述的Person自定义构造函数创建一个实例

let per1 = new Person()

这个过程是这样的

  1. per1实例(对象)内部的[[Prototype]]指针就会被赋值为Person构造函数的原型对象(也就是Person.prototype)
  2. per1可以访问Person的原型上所有属性

js脚本中是没有访问[[Prototype]]特性的标准方式,但是谷歌浏览器火狐还有safari会在每一个对象上暴露__proto__属性,这个属性可以直接访问构造函数的原型也就是这个实例的原型

但是,实例是和构造函数没有直接的联系的,只和构造函数的原型又关联

console.log(Person.prototype === per1.__proto__); // true;

Object的原型的原型是null

console.log(Person.prototype.__proto__ === Object.prototype) // true
console.log(Person.prototype.__proto__.constructor === Object); //true
console.log(Person.prototype.__proto__.__proto__ === null);

1

如图可以看出实例,构造函数,构造函数的原型之间的关系

这里比较重要的点在于

  1. 构造函数的原型(Person.prototype)的constructor属性是指回Person构造函数的
  2. 实例和构造函数之间没有直接联系
  3. 所有通过同一个构造函数构建出来的实例,其中[[Prototype]]指针是指向构造函数的原型。

3. 检查实例原型#

1. instanceof#

由实例直接调用,去判断实例的原型链中是否有某个构造函数的原型

这里一定要注意,是判断某个构造函数的原型

console.log(per1 instanceof Person); // true
console.log(per1 instanceof Object); // true;
console.log(Person.prototype instanceof Object); // true;

如第二大点的图1可以看清这样的继承关系,继承关系另一篇文章再说

2. isPrototypeOf#

这个方法是为了确定两个对象之间原型的关系

因为如第2大点所说,不是所有的实现都对外暴露的[[Prototype]]的指针

所以可以直接在原型上去调用这个方法检测某个对象是否的[[Prototype]]的指针是否指向它,因此也能够检测出两个对象之间的关系

console.log(Person.prototype.isPrototypeOf(per1)); // true;

3. Object.getPrototypeOf()#

这个方法会返回参数的内部[[Prototype]]的指针(这个[[Prototype]]也其实就是参数的特性,可以去一篇叫做对象的属性特性详解的文章查看)

这个函数传入一个 实例, 返回这个实例的原型对象,也就是它的构造函数的原型对象

console.log(Object.getPrototypeOf(per1)===Person.prototype);
Object.getPrototypeOf(per1).name; // weng

4. *Object.setPrototypeOf()#

这个方法可以给实例的[[Prototype]]指针写入一个新的值,这样可以重写一个实例的原型继承关系

let a = {
name: 'weng';
age:23
};
let b = {
name: 'kaimin'
}
Object.setPrototypeof(b, a);
console.log(b.name) // kaimin
console.log(b.age) // 23
console.log(Object.getPrototypeOf(b) === a); // true;

这里不推荐使用这种覆盖实例原型指向的方式改变原型继承的关系,会严重的影响代码的性能。

Mozilla文档中说的“在所有浏览器和js的引擎中,修改继承关系的影响都是微妙而且深远的。这种影响并不是执行以上这个代码那么简单,而是会涉及到所有访问了那些修改过 [[Prototype]]指向的实例的代码”

因为有上述的弊端,可以看另外一篇文章,继承之原型式继承 中讲到的创建新对象的形式,也就是类似于Object.create()来创建一个新对象,同时给他指定一个原型,这块继承会在继承文章中详细解释

let a = {
name: 'weng';
age:23
};
let b = Object.create(a);
b.name = 'kaimin';
console.log(b.name) // kaimin
console.log(b.age) // 23
console.log(Object.getPrototypeOf(b)) // a

Object.create这种形式其实是以匿名构造函数的形式,指定这个匿名函数的原型为你想要的那个实例,因为这里咱们只是在乎这个新创建的实例(这里指的是b)他的[[Prototype]]的指向是否是指向所需对象(这里指的是a)的,完全可以跳过显示创建构造函数的形式,使用这个方法不仅仅不会改变原来实例(这里指的是a)的[[Prototype]]指向,而且又让新实例的原型指向了a,就很棒,这里具体还是去继承那边文章看

4.原型的层级#

例如以下代码

function BasePerson(){
this.country = 'China';
this.age = 23;
}
function Person(){
this.name = 'weng';
}
Person.prototype = new BasePersn();
const per1 = new Person();
per1.address = 'fujian';
console.log(per1.age) // 23

以上代码的各个关系如下图所示;

image-20210530163634192

这里比较重要的是Person.prototype,这里手动给他赋值给了new BasePerson()构造出来的实例,后面又手动的给把他的constructor属性赋值给了Person,这里为什么呢?本文最后会说明

之后呢,Person的原型对象中有了BasePerson构造函数内部初始化的时候的一些参数

按照这上面的层级关系

per1.name; // weng
per1.address; // fujian
per1.age; // 23

这里其实都是可以访问到的

在通过实例访问属性的时候,会按这个属性的名称开始搜索,一层层往上走,一开始搜索实例本身是否有这些属性

,如果有就返回,如果没有继续通过 [[Prototype]]的指针向上搜索原型。

constructor只存在于原型对象上面,其实也可以通过访问实例的原型指针来访问这个constructor

如果有存在同名属性的话,实力上创建的会覆盖原型上的,但不至于改变原型上的同名属性,只是给他盖住看不见了

1.删除实例中的属性#

delete操作符号可以删除实例上面定义的属性值,调用时候,这个实例的原型上的值不会被删除

要删除的话得直接在原型上做操作

例如上面的代码构建的实例per1

delete per1.address;
console.log(per1.address) // undefined

通过删除实例上的属性,可能会暴露出原型上的同名属性,这里的话具体而论,那怎么判断这个属性是在原型上还是在实例上的呢?

2. 判断属性在原型上还是在实例上#

1. hasOwnProperty()#

这个方法可以用来判断某个属性是否在实例上面(这个函数不进入实例的原型中去搜索指定属性)

这个方法直接在实例上调用,继承至Object构造函数的原型对象,如果害怕实例的中有同名方法的话可以使用Object.prototype.hasOwnProperty.call(this)来调用

比如上面的per1实例在没有删除addrss的情况下

per1.hasOwnProperty('address'); // true;
per1.hasOwnProperty('name'); // true;
per1.hasOwnProperty('age'); // false;

per1.age实际上返回的是这个实例原型上的age,hasOwnProperty是访问不到的

那如果要确定是否原型上有这个值呢?不用per1[’…’]来判断

2. in#

in操作符有两种使用方式

  1. 在for-in中使用
  2. 单独使用

这里先说说单独使用in,下面得第5点说for-in

in操作符号可以通过对象访问指定属性时候返回true,无论是在原型上还是在实例上面,都可以访问的到

例如上面的per1对象

console.log('name' in per1); // true
console.log('address' in per1); // true
console.log('age' in per1); // true

3. 确定实例的某个属性不在实例上,只在原型上#

结合第4大点的第二小点的两个操作符,可以确定一个函数

function hasPrototypeProperty(object,name){
return !object.hasOwnProperty(name) && (name in object);
}

4. 获取实例的属性#

1. Object.keys()获取实例上的属性(仅仅是实例上可枚举属性)#

2. for-in遍历实例以及其原型上的所有属性(仅仅是可枚举属性)#

3.Object.getOwnPropertyNames()获取仅仅是实例上的属性(是否可枚举都能获取)#

例如:

正常来说,一个实例的原型对象上的constructor属性都是不可以枚举的,是不能够通过1,2两个方法获取的

let keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys); // [...., 'constructor']

4. Object.getOwnPropertySymbols() 仅仅针对符号针对实例属性(是否枚举都可以)#

let k1 = Symbol('k1');
let o = {
[k1]: 'helloworld'
}
console.log(o);
// [Symbol(k1)]

5. 属性的枚举顺序#

以上可以总结几种获取实例或者实例原型上的一些属性的方法

  1. for-in
  2. Object.keys()
  3. Object.getOwnPropertyNames()
  4. Object.getOwnPropertySymbols();

这里涉及到获取属性的顺序,还有一个Object.assign这类浅层复制对象的方式

for-in以及Object.keys()获取到的属性值的顺序是确定的,取决于js的引擎

Object.getOwnPropertyNames(),Object.getOwnPropertySymbols(); 以及Object.assign得出来的属性的枚举顺序都是确定的。

按照以下规则:

  1. 先升序枚举数值键
  2. 以插入顺序枚举字符串和符号键
  3. 对象字面量中定义的键,以逗号分隔的顺序插入

例如:

let k1 = Symbol('k1'), k2 = Symbol('k2');
let o = {
1: 1,
first:'first',
[k1]:'hello',
second:'second',
0:0
}
o[k2] = 'world';
o[3] = 3;
o.third = 'third';
o[2] = 2;
console.log(Object.getOwnPropertyNames(0));
// ["0","1","2","3","first","second","third"];
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1),Symbol(k2)];

6. 原型中存在的问题#

  1. 弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值
  2. 原型上引用值属性的问题

第一点是显而易见的

第二点中举个例子

function Person(){}
Person.prototype.arr = [1,2,3,4];
let per1 = new Person();
let per2 = new Person();
per1.arr.push(5);
console.log(per2.arr) // [1,2,3,4, 5];

引用类型的arr定义在原型上,这时候对per1上的arr属性进行修改,因为per1实例上不存在arr的属性,那么会找到原型中的arr,这时候通过arr.push是直接作用在原型中的arr的,所以,引用类型属性之间的共享特性导致per2.arr访问的也是原型上的arr,意思就是引用类型访问的都是一个指针,就特么和对象一样。

本篇文章中留下来的疑问#

1. constructor手动赋值的情况#

以上第4大点中的层级关系代码中,手动将原型中的constructor赋值给了Person

function BasePerson(){
this.country = 'China';
this.age = 23;
}
function Person(){
this.name = 'weng';
}
Person.prototype = new BasePersn();
Person.prototype.constructor = Person;
const per1 = new Person();
per1.address = 'fujian';
console.log(per1.age) // 23

因为在这个例子中,Person.prototype被手动设置为一个BasePerson构建函数构建出来的新实例,这个过程相当于重写了Person构造函数的原型,这样重写之后,Person.prototype就不再指向自身的Person,由本文开头说的一样,函数构建的时候会默认创建原型,也就是prototype对象,也会自动给原型的contructor赋值,这个写法完全覆盖了默认的prototype,造成了constructor不再指向自身的构造函数,而是指向了Object构造函数Object(){}

这个时候就不在能够通过constructor属性来识别是什么类型了,还是得用instanceof

再比如下面的代码

function Person(){
}
let f = new Person();
Person.prototype = {
name:'weng',
sayName(){
console.log(this.name);
}
}
f.sayName(); // 报错,sayName is not a function

这是为什么呢?

由于f实例是在重写Person原型之前就已经构建出来了的,它的[[Prototype]]指针指向的原型对象是一开始Person构造函数所指向的,那本身就不存在sayName这个方法,这时候Person的原型被覆盖了,和f一点关系都没有;

这就解释了为什么不能用实例访问constructor来判断类型标识了得用intanceof

那这时候来解决上面手动赋值constructor的问题

如果constructor的值很重要,可以在上述代码中加入

function Person(){
}
let f = new Person();
Person.prototype = {
constructor: Person,
name:'weng',
sayName(){
console.log(this.name);
}
}

这样就可

但是又存在个问题

咱们知道原型上的constructor的属性是不可枚举的,也就是constructor本身这个属性的特性[[Enumberable]]特性是false,但是这样定义的constructor属性是可以枚举的,那这个时候可以看看 那篇 对象属性特性的文章,然后定义这个属性

创建对象之原型模式
https://nollieleo.github.io/posts/创建对象之原型模式/
作者
翁先森
发布于
2021-05-30
许可协议
CC BY-NC-SA 4.0