为什么会有原型与原型链?
虽然object构造函数和对象字面量都可以用来创建单个对象,但是这种方法可能会产生大量重复的代码,为了解决这个问题,出现了工厂模式。
工厂模式
function createPeople(name,age){
var o = {};
o.name = name;
o.age = age;
o.sayName = function(){
console.log(this.name)
}
return o;
}
let p1 = createPeople('yr', 18);
let p2 = createPeople('lyl', 22);
p1.sayName(); //yr
p2.sayName(); //lyl
工厂模式解决了创建多个相似对象的问题,但是没有解决对象识别问题(怎么样知道一个对象的类型,创建自定义构造函数意味着可以将它的实例标识为一种特定类型,就像New Object,New Array一样,我们可以知道创建出的对象是什么类型)
构造函数
function Person(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
console.log(this.name)
}
}
let p1 = new Person('yr',86);
let p2 = new Person('lyl',88);
一般构造函数的函数名首字母大写,为了和普通函数区分。构造函数本身也是函数,只不过可以用来创建对象而已。
作为普通函数调用
function Person(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
console.log(this.name)
}
}
Person('yr',86);//添加到window
new关键字
要创建Person的新实例,必须使用new操作符,会经历以下步骤:
1. 创建一个新对象
2. 将构造函数的作用域赋值给新对象(this指向新对象)
3. 执行构造函数中的代码(为这个新对象添加属性)
4. 返回新对象
p1和p2分别保存着Person的一个不同实例,这两个对象有一个constructor(构造函数)属性,都指向Person。
constructor属性最初是用来标识对象类型的,但是,检测对象类型,还是需要用instanceof
p1和p2都是Object的实例,是因为所有对象都继承自Object
构造函数的问题
每个方法都要在每个实例上重新创建一遍,p1和p2都有一个名为sayName的方法,但是两个方法不是同一个Function的实例,这时候我们就引入了原型实例
在构造函数中定义方法,方法也就是函数,函数是引用类型,每一次new 实例化的时候 都会在堆内存新建一个空间存储函数,这样就造成了内存浪费,在原型上添加方法,那么所的实例化的对象都会共用这个方法,优点就是减小内存占用 利于代码复用。
属性是需要传参的,对应的值是变化的,就放在构造函数中,方法是一模一样的,就放在prototype
原型实例
我们创建的每一个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。那么我们就可以将部分需要的信息直接添加到原型对象中。
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.sayName = function(){
console.log(this.name)
}
let p1 = new Person('yr',86);
let p2 = new Person('lyl',88);
理解原型对象
只要创建一个新函数,就会为该函数创建一个prototype的属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都自动获得一个constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针,Person.prototype.constructor指向Person。通过这个构造函数可以给原型对象添加其他属性和方法。
在脚本中没有标准的方式去访问这个指针[[prototype]],但是在主流浏览器上每个对象都支持一个属性__proto__,这个连接在与实例与构造函数的原型对象之间。
isPrototypeOf
确定实例与构造函数之间的关系
getPrototypeOf
hasOwnPrototype
检测一个属性是存在于实例中,还是原型中
function Person(age){
this.age = age;
}
Person.prototype.name = 'yr'
Person.prototype.sayName = function(){
console.log(this.name)
}
let p1 = new Person(86);
let p2 = new Person(88);
通过这个方法可以清楚的知道什么时候访问的是实例属性,什么时候访问的是原型属性。
也可以得出:相同属性,先访问实例化对象自身的。
in操作符
无论该属性是存在于实例中还是存在于原型中,in操作符都会返回true,只要通过该对象能够访问到,就返回true
更简单的原型语法
当我们要给某个构造函数的原型上添加多个方法或属性时一个个添加不可取,可以使用下面这种方式
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.name = 'yr'
Person.prototype = {
sayName:function(){
console.log(this.name)
},
sayAge:function(){
console.log(this.age)
}
}
let p1 = new Person('yr',86);
let p2 = new Person('lyl',88);
上面代码中本质上是完全重写了默认的prototype,原本Person.prototype的constructor指向Person,这时该属性变成了新对象的constructor(指向了Object的构造函数),所以需要将constructor属性重新指向Person,虽然说一些操作是可以返回想要的结果,但是constructor已经无法确定对象的类型。需要记录保留某方法或者属性出现在谁的原型上。
Person.prototype = {
constructor:Person,
sayName:function(){
console.log(this.name)
},
sayAge:function(){
console.log(this.age)
}
}
但是这样会导致constructor成为可枚举属性,默认是不可枚举的
可以通过Object.defineProperty设置该属性不可以枚举
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype = {
sayName:function(){
console.log(this.name)
},
sayAge:function(){
console.log(this.age)
}
}
Object.defineProperty(Person.prototype,"constructor",{
enumerable:false,
value:Person
})
但是通过这种方式重写整个原型对象,调用构造函数时会为实例添加一个指向最初原型的[[prototype]]指针,而把原型修改为另外一个对象,相当于切断了构造函数与最初原型的联系。
function Person(name,age){
this.name = name;
this.age = age;
}
let p1 = new Person('yr',86);
Person.prototype = {
sayName:function(){
console.log(this.name)
},
sayAge:function(){
console.log(this.age)
}
}
Object.defineProperty(Person.prototype,"constructor",{
enumerable:false,
value:Person
})
let p2 = new Person('lyl',88);
所以这种方式,实例化对象就一定要要放在重写之后。
原型链
只要有继承关系,就会有原型链,每种继承的原型链不同
此时的原型链是最基础原型链
总结
原型
引用类型(对象)除null之外都会有一个__proto__(隐式原型)属性值是一个对象
构造函数都会有一个prototype(显式原型)属性值是一个对象
当访问一个对象的某个属性时,如果这个对象自身没有这个属性,就会去对象的__proto__中找(也就是该对象的构造函数的prototype)
原型链
当我们使用某一个对象的属性和方法的时候,会先在创建对象的构造函数中查找自身
是否拥有改属性,如果没有,会在构造函数的原型对象中查找,如果还是没有,会继续
向继承父级查找,在父类原型中查找,一直到Object基类,直到找到null,但是返回的
是undefined。