js原型于原型链

136 阅读6分钟

原型就是每个对象都拥有一个属性,被称为原型,原型指向的对象称为“原型对象”。原型分为“隐式原型”和“显示原型”。

隐式原型

JavaScript中每一个对象都有一个特殊的内置属性[[prototype]](隐式原型),这个属性指向的对象称为隐式原型对象

早期ECMA规范没有规定如何去查看[[prototype]],但是大部分浏览器给对象提供了__proto__ 属性,可以查看这个对象的原型对象(实际开发不要使用因为有部分游览器没有提供,兼容性有问题)。后期ECMA提供了 Object.getPrototypeOf(obj) ,可以查看原型对象。

原型对象就相当于是对象的父类,当我们获取对象没有的属性时,对象就会自动去原型对象中查找(沿着原型链查找)

代码示例:

let obj = {name: "curry"}
​
//这里借用下__proto__展示案例,实际开发一定不要使用
obj.__proto__.age = 18;
​
console.log(obj.age);

最终还是能打印age属性结果为18,因为对象在原型对象中找到了age。

显示原型

函数也是属于对象,也拥有属性,也有隐式原型([[prototype]])。但是函数还有一个属性prototype(显示原型),这个属性指向的对象称之为显示原型对象,prototype没有兼容性问题,可以直接使用。

原型的使用体现在,在new关键字调用构造函数的时候,会把新创建的对象的[[prototype]]属性设置为函数的prototype属性(把新对象的隐式原型指向函数的显示原型)。设置好后,通过对象.__proto__ === 构造函数.prototype 可以发现返回值为true,说明了他们指向的就是同一个对象,同一块内存。

代码示例:

function Fn(){}
​
var f1 = new Fn();
var f2 = new Fn();
​
console.log(f1.__proto__ === Fn.prototype);
console.log(f2.__proto__ === Fn.prototype);

原型在内存中的表现

8.原型对原型对象在内存中的表现.jpg

通过同一个构造函数创建的对象的原型([[prototype]])都是指向同一个对象,这个对象就是构造函数的原型(prototype指向的对象) 。既然他们都指向了同一个对象,那么f1修改了原型,f2的原型也会改变。

代码示例:

function Fn(){}
​
let f1 = new Fn();
let f2 = new Fn();f1.__proto__.name = "kd" //实际开发不要通过对象操作原型
console.log(f2.name); //kd

之际开发中我们不会通过对象的__proto__属性在原型上定义属性,因为有兼容性问题,而是通过构造函数访问原型进行修改

代码示例:

function Fn(){}
​
let f1 = new Fn();
let f2 = new Fn();
​
Fn.prototype.name = "curry"console.log(f1.name);//curry
console.log(f2.name);//curry

补充:

看到这里如果你还没原型和原型对象之间的关系,我就再解释一下。下面代码有一个obj对象,名字(name)就是对象的一个属性,被称为名字,而名字指向的对象,称为“名字对象”(对应了开头第一句话:原型就是对象一个属性,被称为原型,原型指向的对象称为“原型对象”。

let obj = {
    name: {
    }
}

原型对象的constructor属性

刚刚从上面的图片我们发现原型对象还有一个constructor属性,这个属性是ECMA规定必须添加的,这个属性没有什么作用,他指向的是构造函数本身。从图中也能看出这样他们形成了循环引用,我们甚至可以这样访问Fn.prototype.constructor.prototype...

代码示例:

function Fn(){}
​
Fn.prototype.name = "curry";
​
console.log(Fn.prototype);
​
console.log(Object.getOwnPropertyDescriptor(Fn.prototype,"constructor"));//enumerable为false

由于原型对象的constructor属性的属性描述符中的enumerable默认为false,所以在node打印结果为{name: "curry"},不会显示constructor属性,游览器虽然都会显示,但constructor颜色相对其他属性较淡。

修改原型

我们也可以手动修改原型,使得原型指向另外一个对象,如下代码:

function Fn(){}
​
Fn.prototype = {
    constructor: Fn,
    name: "kd"
}
​
let f1 = new Fn();
​
console.log(f1.name);

上面这段代码看似没问题,其实constructor是有问题的,前面有说过constructor的属性描述符中的enumerable默认为false,现在打印发现enumerable为true。

function Fn(){}
​
Fn.prototype = {
    constructor: Fn,
    name: "kd"
}
​
console.log(Object.getOwnPropertyDescriptor(Fn.prototype, "constructor"));

所以原型对象修改后,constructor属性不能直接写入原型对象中,要借用Object.defineProperty()插入,代码如下:

function Fn(){}
​
Fn.prototype = {
    name: "kd"
}
​
Object.defineProperty(Fn.prototype, "constructor", {
    value: Fn,
    configurable: true,
    enumerable: false,
    writable: true,
});
​
console.log(Object.getOwnPropertyDescriptor(Fn.prototype, "constructor"));

原型的作用

原型有如下两大作用

  • 把对象的方法放到原型对象中,方法共享,节省空间
  • 继承(继承内容较多,需要原型链的知识,这里不讲)

方法需要定义到原型对象中

如下Fn是构造函数,每次new出来的对象内部都有属于自己的函数(playing),f1有f2也有,这样每次创建对象都会为playing函数在内存中开辟新的空间。

function Fn(name){
    this.name = name;
    this.playing(){
        console.log(this.name + "在打篮球");
    }
}
​
let f1 = new Fn("curry");
let f2 = new Fn("kd");

既然构造函数与构造出来的对象都指向了同一个原型对象,为了减少内存的使用,我们就需要把方法定义到原型上,这样就不用在每次在构造对象的时候为函数开辟空间,需要使用的时候去原型上调用该方法即可。可将上面代码修改为如下代码:

function Fn(name){
    this.name = name;
}
Fn.prototype.playing = function(){
    console.log(this.name + "在打篮球");
}
​
let f1 = new Fn("curry");
let f2 = new Fn("kd");
f1.playing();
f2.playing();

我们也不用担心定义到原型上的方法,this指向有问题,因为this的指向是代码执行时才会确定(this是动态绑定的),从f1.playing();f2.playing();执行可以看出this执行没有问题,因为通过f1调用this就会默认指向f1对象,通过f2调用this指向f2(不明白this指向的可以看我之前的文章”彻底明白js中的this指向“)。

总结: 同一个构造函数创建的对象,不需要为每个对象添加相同的方法,因为方法是不变的,我们需要把方法定义到原型上,减少内存使用

属性不用定义到原型对象中

那么属性为什么不用定义到原型上呢?看如下代码就知道,因为属性是要被赋值的,而且修改获取是同一个原型对象的属性,这样f1的时候原型对象name赋值为了"curry",f2的时候原型对象name又变成了"kd"。

function Fn(name){}
Fn.prototype.name = name;
​
let f1 = new Fn("curry");
let f2 = new Fn("kd");
​
console.log(f1.name);//kd

总结:属性是需要变化的,只能属于自身,不能放在原型上让其他对象使用造成改变