原型模式
每一个函数都会创建一个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(); // weng1.与构造函数的不同
原型模式是直接再构造函数的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()这个过程是这样的
- per1实例(对象)内部的[[Prototype]]指针就会被赋值为Person构造函数的原型对象(也就是Person.prototype)
- per1可以访问Person的原型上所有属性
js脚本中是没有访问[[Prototype]]特性的标准方式,但是谷歌浏览器火狐还有safari会在每一个对象上暴露
__proto__属性,这个属性可以直接访问构造函数的原型也就是这个实例的原型
但是,实例是和构造函数没有直接的联系的,只和构造函数的原型又关联
console.log(Person.prototype === per1.__proto__); // true;Object的原型的原型是null
console.log(Person.prototype.__proto__ === Object.prototype) // trueconsole.log(Person.prototype.__proto__.constructor === Object); //trueconsole.log(Person.prototype.__proto__.__proto__ === null);
如图可以看出实例,构造函数,构造函数的原型之间的关系
这里比较重要的点在于
- 构造函数的原型(Person.prototype)的constructor属性是指回Person构造函数的
- 实例和构造函数之间没有直接联系
- 所有通过同一个构造函数构建出来的实例,其中
[[Prototype]]指针是指向构造函数的原型。
3. 检查实例原型
1. instanceof
由实例直接调用,去判断实例的原型链中是否有某个构造函数的原型
这里一定要注意,是判断某个构造函数的原型
console.log(per1 instanceof Person); // trueconsole.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; // weng4. *Object.setPrototypeOf()
这个方法可以给实例的[[Prototype]]指针写入一个新的值,这样可以重写一个实例的原型继承关系
let a = { name: 'weng'; age:23};let b = { name: 'kaimin'}
Object.setPrototypeof(b, a);
console.log(b.name) // kaiminconsole.log(b.age) // 23console.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) // kaiminconsole.log(b.age) // 23console.log(Object.getPrototypeOf(b)) // aObject.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以上代码的各个关系如下图所示;

这里比较重要的是Person.prototype,这里手动给他赋值给了new BasePerson()构造出来的实例,后面又手动的给把他的constructor属性赋值给了Person,这里为什么呢?本文最后会说明
之后呢,Person的原型对象中有了BasePerson构造函数内部初始化的时候的一些参数
按照这上面的层级关系
per1.name; // wengper1.address; // fujianper1.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操作符有两种使用方式
- 在for-in中使用
- 单独使用
这里先说说单独使用in,下面得第5点说for-in
in操作符号可以通过对象访问指定属性时候返回true,无论是在原型上还是在实例上面,都可以访问的到
例如上面的per1对象
console.log('name' in per1); // trueconsole.log('address' in per1); // trueconsole.log('age' in per1); // true3. 确定实例的某个属性不在实例上,只在原型上
结合第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. 属性的枚举顺序
以上可以总结几种获取实例或者实例原型上的一些属性的方法
- for-in
- Object.keys()
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols();
这里涉及到获取属性的顺序,还有一个Object.assign这类浅层复制对象的方式
for-in以及Object.keys()获取到的属性值的顺序是确定的,取决于js的引擎
Object.getOwnPropertyNames(),Object.getOwnPropertySymbols(); 以及Object.assign得出来的属性的枚举顺序都是确定的。
按照以下规则:
- 先升序枚举数值键
- 以插入顺序枚举字符串和符号键
- 对象字面量中定义的键,以逗号分隔的顺序插入
例如:
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. 原型中存在的问题
- 弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值
- 原型上引用值属性的问题
第一点是显而易见的
第二点中举个例子
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属性是可以枚举的,那这个时候可以看看 那篇 对象属性特性的文章,然后定义这个属性