原型与原型链

124 阅读5分钟
原文链接: github.com

思维导图

什么是构造函数

JavaScript语言使用构造函数作为对象的模板,描述实例对象的基本结构,,实例对象的属性和方法,可以定义在构造函数内部,一个构造函数,可以生成多个实例对象。其实构造函数本身就是一个普通的函数,为了区分普通函数,我们通常将构造函数的首字母大写。

function Person(name) {
    this.name = name;
    this.sayHi = function () {
        console.log('大家好,我是' + this.name);
    };
     this.walk = function() {
     	console.log("walk");
    };
}   
let person1 = new Person(‘张宇’);
let person2 = new Person('张三');
person1.sayHi();		//大家好,我是张宇						
person2.sayHi();		//大家好,我是张三

上述代码中,Person 就是一个构造函数,同时我们使用new 方法生成了两个实例对象 person1 person2

prototype

由于person1person2 都是由同一个构造函数的两个实例, 因此它们拥有构造函数Person 的属性和方法

person1.walk();         //walk
person2.walk();         //walk
console.log(person1.walk === person2.walk);     //false

上述代码可以看出来,person1person2 都拥有一个相同的方法walk , 但却不是同一个方法,也就是说,每创建一个实例,都会在内存中新建一个walk方法 ,这样会十分浪费系统资源,因为所有的walk方法都是一样的,应该完全共享。

这时候就到了 prototype出场的时候了,那么什么是prototype

JavaScript 语言规定,每个函数都会有一个 prototype 属性,指向一个对象

console.dir(Person);

打印构造函数Person 时发现确实有prototype属性,当构造函数生成实例时。prototype属性会自动成为实例对象的原型,JavaScript 继承机制的设计思想是,原型对象的所有属性和方法,都能被实例对象继承,也就是说,如果将对象的属性和方法定义在原型上,就能解决上述问题,从而节省系统资源。

function Person(name) {
    this.name = name;
}
Person.prototype.walk = function() {
    console.log("walk");
}
let person1 = new Person("张宇");
let person2 = new Person("张三");

console.log(person1.hasOwnProperty('walk'));    //false
person1.walk();         //walk
person2.walk();         //walk
console.log(person1.walk === person2.walk);     //true

上述代码中,通过hasOwnProperty0方法可以看出,person1本身并没有walk()方法 ,因为walk是添加在原型对象的方法,所有的实例对象都共享了该方法。原型对象的属性和方法不是实例对象的属性和方法

但如果实例对象本身就拥有这个属性或方法,那么就不会再去原型对象寻找这个属性和方法

person1.walk = function () {
    console.log('run')
}
person1.walk();         //run

上述代码中可以看出,当我给实例对象person1添加了walk方法后,他就不再去原型对象中寻找这个方法了。也就是说,只有当实例对象本身没有某个属性或者方法的时候,它才会去原型对象寻找某个属性和方法。

当我们打印Person.prototype时,可以看出他有constructor属性和__proto__属性

console.dir(Person.prototype);

constructor

这个属性默认指向prototype对象所在的构造函数

console.log(Person.prototype.constructor === Person);   //true
console.log(person1.constructor === Person);   //true
console.log(person1.hasOwnProperty('constructor'));     //false

由于constructor是定义在prototype对象上面的属性,因此每一个实例对象都可以调用,我们在实例对象上调用该属性,其实等同于prototype对象直接调用

正是因为这个特性,我们可以实现从一个实例对象上创建一个新的实例对象

let person3 = new person1.constructor('李四');
console.log(person3.name);      //李四·

constructor属性表示的是prototype对象与构造函数之间的关系,因此当修改原型对象是,最好将同时修改constructor属性,不然会照成一些不必要的问题出现

Person.prototype = {
    swim: function() {
        console.log('swim');
    }
}
let person4 = new Person('王五');
person4.swim();         	//swim
// person1.swim();         //person1.swim is not a function
//由于我们直接修改了Person.prototype,修改前的实例对象则还指向原对象,因此实例person1无法访问到 swim方法,
console.log(person1.__proto__ === person4.__proto__); 	//false

上述代码我们可以发现,person1person4的原型对象不是指向同一个。

因此,我们最好只在原型对象中添加属性方法,而不是直接进行修改。

__proto__

每个实例对象都有__proto__属性,该属性返回对象原型。

function Person(name) {
    this.name = name;
}
let person = new Person('张宇'); 
console.log(person.__proto__ === Person.prototype);  //true

MDN是这样解释它的

Object.prototype__proto__ 属性是一个访问器属性(一个getter函数和一个setter函数), 暴露了通过它访问的对象的内部[[Prototype]] (一个对象或 null)。

使用__proto__是有争议的,也不鼓励使用它。因为它从来没有被包括在EcmaScript语言规范中,但是现代浏览器都实现了它。__proto__属性已在ECMAScript 6语言规范中标准化,用于确保Web浏览器的兼容性,因此它未来将被支持。它已被不推荐使用, 现在更推荐使用Object.getPrototypeOf/ Reflect.getPrototypeOfObject.setPrototypeOf/ Reflect.setPrototypeOf(尽管如此,设置对象的[[Prototype]]是一个缓慢的操作,如果性能是一个问题,应该避免)。

proto 属性也可以在对象文字定义中使用对象[[Prototype]]来创建,作为Object.create()的一个替代。

原型链

JavaScript 规定,每个对象都有自己的原型,且可以充当其他对象的原型。

对象的原型也是对象,那么它也会有自己的原型,这样就会形成一条“原型链”,对象---原型---原型的原型----

所有的原型对象最终都可以追溯到Object.prototype对象,而Object.prototype也是个对象,那它的原型是什么?

console.log(Object.prototype.__proto__);        //null

上述代码得知,Object.prototype对象的原型是 null,null 没有自己的原型,也没有任何属性和方法,因此,原型链的尽头就是null。

之前内容我们得知,当我们读取对象的属性和方法时,会先在自身读查找,如果没有就到其原型查找,如果还是没有,则到其原型的原型查找,直到Object.prototype对象,如果还是找不到,则返回undefined,因此,我们得知,所有的对象都会继承Object.prototype对象的属性和方法,如果对象和其原型都定义了一个同名属性或方法,则优先读取对象自身的属性和方法,覆盖掉其原型的属性和方法。

根据上述内容我们可以的到以上关系图(其中橙色线为原型链)

之前我们说过,构造函数本质上也是一个函数,那么它是不是也有着自己的原型?

console.log(Person.__proto__ === Function.prototype);  //true
//Function.prototype 也是对象
console.log(Function.prototype.__proto__ === Object.prototype); //true

根据上述内容我们又可以得到以下关系图,两者结合就是完整的关系图