1.理解原型
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性(指向原型对象),默认情况下,所有原型对象自动获得一个名为construcor的属性,指回与之关联的构造函数,
function Person(){ }
Person.prototype.name='Nicholas';
Person.prototype.age=29;
Person.prototype.sayName=function(){
console.log(this.name)
};
let person1=new Person();
Person1.sayName(); //Nicholas
let person2=new Person();
person2.sayName( ); //"nicholas"
console.log(person1.sayName == person2.sayName); //true
对前面的例子而言,Person.prototype.constructor指向Person 然后,因构造函数而异,可能会给原型对象添加其他属性和方法
在自定义构造函数时,原型对象默认只会获得construcotr属性,其他的所有方法都继承自Object.每次调用构造函数创建一个新实例,这个实例的内部[[construcotr]]指针就会被赋值为构造函数的原型对象,脚本中没有访问这个[[Prototype]]特性的标准方式,但Firefox.Safari和Chrome会在每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型,在其他实现中,这个特性完全被隐藏了,关键在于理解这一点: 实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有
可以通过下面的代码来理解原型的行为:
//构造函数
function Person(){ }
//声明之后,构造函数就有了一个与之关联的原型对象:
console.log(typeof Person.prototype);console.log(Person.prototype) //谷歌 {
construcotr: f Person(),
__proto__: Object
}
如前所述,构造函数有一个prototype属性 引用其原型对象,而这个原型对象也有一个 construcotr属性,引用这个构造函数 换句话说,两者循环引用:
console.log(Person.prototype.construcotr === Person); //true
正常的原型链都会终止与Object的原型对象 Object 原型的原型是null
//原型对象的__proto__== 构造函数的原型对象 因为Person.prototype本身是由Object构造函数创建的对象,所以Person.prototype.__proto__是指向Object.prototype
console.log(Person.prototype._proto__ === Object.prototype); //true
//构造函数的原型对象的construcotr===构造函数 console.log(Person.prototype._proto_.constructor===Object); //true
//Object.prototype._proto_ ==null
conosle.log(Person.prototype._proto_._proto_===null)
console.log(Person.prototype._proto_);
let person1 = new Person( ),
person2 = new Person( );
构造函数,原型对象和实例时3个完全不同的对象:
console.log(person1 !== Person); //true
console.log(person1 !== Person.prototype); //true
console.log(Person.prototype !== Person); //true
实例通过_proto_链接到原型对象,它实际上指向隐藏特性[[Prototype]]构造函数通过prototype属性链接到原型对象,实例与构造函数没有直接联系,与原型对象有直接联系
console.log(person1._proto_ === Person.prototype); //true
console.log(person1._proto_.construcotor)===Person //true
同一个构造函数创建的两个实例,共享同一个原型对象:
console.log(person1._Proto_) === person2._proto_; //true
instanceof检查实例的原型链中
//是否包含指定构造函数的原型
console.log(person1 instanceof Person); //true
console.log(person1 instanceof Object); //true
console.log(Person.prototoype instanceof Object); //true
注意:
- Person.prototype指向原型对象,而Person.prototype.contructor指回Person构造函数
- 两个实例person1和person都只有一个内部属性指回Person.prototype,而且两者与构造函数没有直接联系
- 虽然这个两个实力都没有属性和方法,但person1.sayName()可以正常调用,这是由于对象属性查找机制的原因
isPrototoype(object): 判断某个对象是否是另一个对象原型链中的一个节点。
object: 你要检查的目标对象
console.log(Person.prototype.isPrototypeOf(person1)); //true
console.log(Person.prototype.isPrototypeOf(person2)); //true
//因为这两个例子内部都有链接指向Person.prototype
Object.getPrototypeOf( ), 返回参数的内部特性[[Prototype]]的值,
console.log(Object.getPrototypeOf(person1)=== Person.prototype); //true
console.log(Object.getPrototypeOf(person1).name); //Nicholas
2.原型层级
在通过对象访问属性时,会按照这个属性的名称开始搜索,搜索开始于对象实例本身,如果在这个实力上发现了给定的名称,则返回该名称对应的值,如果没有找到这个属性,则搜索会沿着指针进入原型对象,然后在原型对象上找到属性后,再返回对应的值,
在调用person1.sayName()时,会发生两步
- 首先,js引擎会问person1 实例有sayName属性吗? 没有
- 然后, person1的原型有sayName属性吗? 有
在调用person2,sayName()时,会发生同样的搜索过程,而且也会返回相同的结果,这就是原型用于在多个对象实力间共享属性和方法的原理
虽然可以通过实例读取原型对象上的值,但不可能通过实例重写这些值,如果在实例上添加了一个与原型对象中同名的属性,那就会在实例上创建这个属性,这个属性会遮住原型对象上的属性
function Person(){ }
Person.prototype.name='Nicholas';
Person.prototype.age=29;
Person.prototype.sayName=function(){
console.log(this.name)
};
let person1=new Person();let person2=new Person();
console.log(person1.hasOwnProperty('name')); //falseconsole.log('name' in person1) //true
person1.name="Greg";console.log(person1.name); //Greg 来自实例
//in操作符
console.log('name' in person1) //true
console.log(person2.hasOwnProperty('name')); //true
console.log(person2.name); //Nicholas 来自原型
hasOwnProperty():用于确定某个属性实在实例还是在原型对象上,这个方法是继承自Object的,会在属性存在于调用它的对象实例上时返回true
注意: js的Object.getOwnPropertyDescriptor( )方法只对实例属性有效,要取得原型属性的描述符,就必须直接在原型对象上调用Object.getOwnProperty-Descriptor().
3.原型和in操作符
有两种方式使用in操作符: 单独使用和在for-in循环使用时,in操作符会在可以通过对象访问指定属性时返回true,无论该属性是在实例上还是在原型上,以上面为例
在for-in 循环中使用 in 操作符时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性, 遮蔽原型中不可枚举([[Enumerable]]特性被设置为false)属性的实例属性也会在for-in循环中返回,因为默认情况下开发者定义的属性都是可枚举的
Object.keys()方法, 接收一个对象作为参数,返回包含该对象所有可枚举属性名称的字符串数组
console.log(Object.keys(Person.prototype)) // [name,age,sayName]
let p1=new Person();
p1.name="Rob";
p1.age=31
console.log(Object.keys(p1)) //[name,age]
在Person的实力上调用时,Object.keys()返回的数组中只包含"name"和"age";两个属性
Object.getOwnPropertyNames(); 无论是否可以枚举,列出所有实例属性
let keys=Object.getOwnPropertyNames(Person.prototype);
console.log(keys); '[constructor,age,name,sayname]'
注意: 返回的结果中包含了一个不可枚举的属性construcotor,Object.keys()和object. getOwnPropertyNames()在适当的时候都可用来代替for-in循环
在es6新增符号类型后,相应地出现了增加了恶一个Object.getOwnPropertyNames()的兄弟方法的需求,因为以符号为键的属性没有名称的概念,因此,Object.getOwnProperty-Symbols()方法就出现了,这个方法与object.getOwnPropertyNames()类似,只是针对符号而已:
let k1=Symbol('k1'),
k2= Symbol('k2');
let o={
[k1]:'k1',
[k2]:'k2'
};
console.log(Object.getOwnPropertySymbol(o));
//[Symbol(k1),Symbol(k2)]
4.其他原型语法(重写原型)
function Person(){ }
Person.prototype={
name:'Nicholas',
age:29,
sayName(){
console.log(this.name)
}
}
注意: 这样重写之后,Person.prototype的construcotr属性就不指向Person了,在创建函数时,也会创建它的Prototype对象,同时会自动给这个原型的construcotr属性赋值,而上面的写法完全重写了默认的prototype对象,因此其constructor属性也指向了完全不同的新对象(Object 构造函数),不再指向原来的构造函数,
5.原型的动态性
因为从原型上搜索值的过程是动态的,所以即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来
let firend = new Person( );
Person.prototype.sayHi=function(){
conosle.log('HI')''
};
friend.sayHi(); //hi
调用friend.sayHi发生: 在这个实例中搜索名为sayHi的属性,没有找到,继续搜索原型对象,因为实例和原型之间的连接就是简单的指针,而不是保存的副本,所以会在原型上找到sayHi并返回这个属性
注意:
1.这个跟重写是两回事, 实例的[[Prototype]]的指针是在调用构造函数时自动复制的,这个指针即使把原型修改为不同的对象也不会变
2. 重写整个原型会切断最初原型与构造函数的联系,但实例引用的是最初的原型,记住,实例只有指向原型的指针,没有指向构造函数的指针
function Person(){ } let friend =new Person(); Person.prototype={ constructor: Person, name:"Nicholas", age:29, job:'Software Engineer' sayName(){ console.log('11111') } } friend.sayName(); //错误
在这个例子中,Person的新实例是在重写原型对象之前创建的,在调用friend.SayName()时候,会导致错误,这是因为firend指向的原型还是最初的原型,而这个原型上并没有sayName属性
重写构造函数上的原型之后再创建的实例才会应用新的原型,而在此之前创建的实例仍然会引用最初的原型
6.原型对象原型
原型模式之所以重要,不仅提现在自定义类型上,而且还因为它也是实现所有原生引用类型的模式
所有原生引用类型的构造函数(包括Object,Array,String等)都在原型上定义了实例方法,比如,数组的实例的sort()方法就是Array.prototype上定义的,而字符串包装对象的substring()方法也是在String.prototype上定义的
下面的代码给string原始值包装类型的实例添加了一个startsWith()方法:
string.prototype.startsWith=function(text){
return this.indexOf(text) ===0;
};
let msg='Hello world!';
console.log(msg.startsWith('Hello')); //true
注意:不推荐这样做,可能会引发命名冲突, 引发误会, 推荐的做法是创建一个自定义的类继承原生类型
7.原型链
其实就是一种查找机制,用于实现对象之间的继承关系,当访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,js引擎会沿着原型链向上查找,知道找到对应的属性或方法或者到达原型链的顶端(即Object.prototype). 通过原型链,js实现了基于原型的继承机制,这是的对象之间可以共享属性和方法,同时也保留了各自的特定属性和方法
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function () {
console.log("Hello, I am " + this.name);
};const p1 = new Person("Tom");
p1.sayHello(); // Hello, I am Tom
//原型链
p1 --> Person.prototype --> Object.prototype --> null
8.原型的问题:
-
弱化了向构造函数传递初始参数的能力,会导致所有实例默认都取得相同的属性值
-
共享特性: 原型上所有属性都是在实例间共享的,这对函数来说比较合适,另外包含原始值的属性也还好,可以通过在实例上添加同名属性来简单的遮蔽原型上的属性,真正的问题来自包含引用值的属性
-
function Person( ){ }
Person.prototype={constructor: Person,
friends: ['Shelby','court']
}
let person1=new Person();
let person2=new Person();
person1.friends.push('van');
conosle.log(person1.friends); //Shelby,Court,Vanconsole.log(person1.friends === person2.friends); //true
由于这个friends属性存在于Person.prototype 而非person1上,新加的这个字符串也会在(指向同一个数组的)person2.firends上反映出来,如果这是有意在多个实例共享数组,那没什么问题,但一般来说,不同的实例应该有属于自己的属性副本,这就是实际开发中通常不单独使用原型模式的原因