1.原型
class Person{
constructor(name,age){
this.name = name
this.age = age
}
run = function(){
console.log("他会跑");
}
eat(){
console.log("他会吃");
}
}
let p = new Person("李华",20)
console.log(p);//Person {name: '李华', age: 20, run: ƒ}
console.log(p.eat);//ƒ eat(){console.log("他会吃");}
从代码和控制台打印的Person实例中可以看到,eat方法没有显示在Person实例里,但是能够访问到eat方法。那eat方法究竟去哪里了,我们看对象的内存空间。
每个对象都有一个隐含的__proto__属性,用来存储其他对象,而被存储的这个对象就是原型对象。当然,原型对象也是一个对象,它就也有__proto__属性, 所有的原型对象最终都指向Object原型,Object原型的__proto__为null。
class Person{
constructor(name,age){
this.name = name
this.age = age
}
run = function(){
console.log("他会跑");
}
eat(){
console.log("他会吃");
}
}
let p = new Person("李华",20)
console.log(p.__proto__);//{constructor: ƒ, eat: ƒ}
console.log(p.__proto__.__proto__);
/* constructor: ƒ, __defineGetter__: ƒ,
__defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …} */
console.log(p.__proto__.__proto__.__proto__);//null
从上面代码可以看到,eat方法在原型对象里。
对象中存储属性的区域实际有两个:
1)对象自身
- 直接通过对象所添加的属性,位于对象自身中
- 在类中通过 x = y 的形式添加的属性,位于对象自身中(如run = function(){ })
2)原型对象 (prototype)
- 对象中还有一些内容,会存储到其他的对象里(原型对象)
- 在对象中会有一个属性用来存储原型对象,这个属性叫做__proto__
- 原型对象也负责为对象存储属性,当我们访问对象中的属性时,会优先访问对象自身的属性,对象自身不包含该属性时,才会去原型对象中寻找
会将对象属性添加到原型对象中的情况:
1)在类中通过xxx()方式添加的方法,位于原型中(如代码中的eat(){ }方法)2)主动向原型中添加的属性或方法访问一个对象的原型对象
访问一个对象的原型对象
- 对象.__proto__
- Object.getPrototypeof(对象)
原型对象中的存在的数据
- 对象中的数据(属性、方法等)
- constructor (对象的构造函数)
class Person{ constructor(name,age){ this.name = name this.age = age } run = function(){ console.log("他会跑"); } eat(){ console.log("他会吃"); } } let p = new Person("李华",20) console.log(p.__proto__.constructor); console.log(p.constructor); /*底下是打印出来的constructor,两个打印的都是同一个实例对象, 这是因为原型对象上的属性可以直接通过对象拿到,如eat方法在原型对象中,p依旧可以拿到。 这是因为原型链的存在。 */ /* class Person{ constructor(name,age){ this.name = name this.age = age } run = function(){ console.log("他会跑"); } eat(){ console.log("他会吃"); } } */
注意:
- 前面说到,原型对象也有原型,这样一层逃一层,就构成了一条原型链,根据对象的复杂程度不同,原型链的长度也不同
- p对象的原型链: p对象 --> Person原型--> Object原型 --> null
- let obj = { },obj对象的原型链: obj对象 --> Object原型 --> null
原型链:
读取对象属性时,会优先对象自身属性,如果对象中有,则使用,没有则去对象的原型中寻找,如果原型中有,则使用,没有则去原型的原型中寻找,直到找到Object对象的原型 (Object的原型没有原型 (为null )),如果依然没有找到,则返回undefined。
作用域链,是找变量的链,找不到会报错
原型链,是找属性的链,找不到会返回undefined
相同类的对象,它们的原型对象都是同一个,也就是说,相同类的对象的原型链是一样的。
如下面的person类的例子。
class Person{ constructor(name,age){ this.name = name this.age = age } } let p = new Person("李华",20) let p2 = new Person("李红",18) console.log(p.__proto__); console.log(p2.__proto__); console.log(p.__proto__ === p2.__proto__);
原型的作用
- 原型就相当于是一个公共的区域,可以被所有该类实例访问,可以将一个该类实例中,所有的公共属性(方法) 统一存储到原型中,这样我们只需要创建一个属性,即可被所有实例访问。
- 在对象中有些值是对象独有的,像属性(name,age,gender) 每个对象都应该有自己值,但是有些值对于每个对象来说都是一样的,像各种方法,对于一样的值没必要重复的创建,就可以在原型中创建。
修改原型
大部分情况下,我们是不需要修改原型对象
注意:
千万不要通过类的实例去修改原型
- 通过修改一个实例对象影响所有同类对象,这么做不合适
- 修改原型先得创建实例,麻烦
- 危险
除了通过__proto _能访问对象的原型外,还可以通过类(构造函数)的prototype属性,来访问实例的原型,修改原型时,最好通过类去修改。
好处:
- 一修改就是修改所有实例的原型
- 无需创建实例即可完成对类的修改
原则:
- 原型尽量不要手动改
- 不要通过实例对象去改
- 通过 类.prototype.属性 去修改
- 不要直接给prototype去赋值(如Object.prototype = { })
总结:
- 原型一般用来创建公共的属性或者方法,比如vue需要每个组件都能用的方法,那么直接把该方法添加到其原型上,Vue.prototype.add = function(){ }。
- 当对象访问属性时,先在自身找,没找到就到自己的原型去找,没找到就去原型的原型找,直到Object原型(null),还没找到就返回undefined,如果中途找到了,就直接返回。
- 除了Object.create(null)创建的对象没有原型,其他如字面量创建,new操作符创建的对象都会有一个__proto__属性依次指向原型对象,直到顶端Object原型null。
- constructor:除了Object.create(null)创建的对象没有constructor,其他对象都有constructor属性,它返回一个构造它的构造函数,如下。
// 创建对象的形式有三种,字面量,new,Object.create() // 字面量形式创建是new创建的简洁版,所以对象o的constructor是它的构造函数Object const o = {} o.constructor === Object // true const o1 = new Object o1.constructor === Object // true const a = [] a.constructor === Array // true const a1 = new Array a1.constructor === Array // true
- prototype:它是类(构造函数)的属性,通过它也可以访问到原型对象,也就是说,实例对象的__proto__属性全等于类(构造函数)的prototype属性。
let obj = {} console.log(obj.__proto__ === Object.prototype);//true class Per{} let a = new Per() console.log(a.__proto__ === Per.prototype);//true
*原型在控制台表示为 [[Prototype]]: 原型对象
ES5的类(构造函数)与ES6的类的区别
// ES5的类,构造函数 function Animal(name,age){ this.name = name, this.age = age } Animal.prototype.eat = function(){ //原型上的方法 console.log("吃东西"); } // 静态属性和方法只能由类本身调用 Animal.weight = 800 //静态属性 Animal.say = function(){ // 静态方法 console.log("你好"); } let a = new Animal("狗",4) console.log(a);//Animal {name: '狗', age: 4} Animal.say()//你好 // ES6的类 class Animals{ constructor(name,age){ this.name = name, this.age = age } static weight = 800 //静态属性 eat(){//原型上的方法 console.log("吃东西"); } static say(){ // 静态方法 console.log("你好"); } } let b = new Animals("猫",2) console.log(b);//Animals {name: '猫', age: 2} Animals.say()//你好
一句话概括,类就是构造函数的语法糖,虽然写法不同,原理是一抹一样。
2.继承
继承的本质就是把父类设置为子类的原型,子类就可以随便用父类的属性和方法,如果不想用,在子类重新设置属性或方法就可以覆盖原型的(父的)属性或方法。
// ES5实现继承
function Animal(){
this.name = "狗剩"
}
function Dog(){
}
Dog.prototype = new Animal()
let p = new Dog()
console.log(p);//[[Prototype]]:Animal
console.log(p.name);//狗剩
// ES6实现继承
class Person{
constructor(name,age){
this.name = name,
this.age = age
}
}
class Per extends Person{
// 这里不设置属性,相当于把父类代码复制下来,其实就是把父类设为子类原型
}
let p2 = new Per("张三",12)
console.log(p2);//Per {name: '张三', age: 12} [[Prototype]]: Person
class Person{
constructor(){
this.name = "二狗"
this.age = 13
}
}
class Per extends Person{
// 如果这里有constructor,整个父类的constructor里的属性就访问不到了
// 因为先访问对象,如果找不到属性再去原型,constructor出现意味着不会去原型找
// 要想再次拿到父类的属性,传入super()参数即可
constructor(age){
super(name)//相当调用父类构造函数,传入的参数就是父类的参数
this.age = age//对父类的某属性重新赋值,或新增属性
}
}
let p2 = new Per(4)
console.log(p2);//Per {name: '二狗', age: 4}
3.new操作符在创建对象的时候干了什么?
- 创建一个普通的JS对象 (Object对象 )
- 将构造函数的prototype属性设置为新对象的原型
- 使用实参来执行构造函数,并且将新对象设置为函数中的this
- 如果构造函数的返回值是一个原始值或者没有指定返回值,则新的对象将会作为返回值返回,如果构造函数返回的是一个非原始值,则该值会作为new运算的返回值返回。通常不会为构造函数指定返回值。