原型与原型链
JavaScript 中的每个对象都有一个原型 (prototype) 属性,它指向另一个对象,这个对象的属性和方法可以被继承。如果在一个对象上调用一个属性或者方法,而这个对象本身没有这个属性或者方法,那么 JavaScript 就会从它的原型对象上查找这个属性或者方法,如果原型对象上也没有找到,则会继续查找原型对象的原型对象,直到找到 Object.prototype 为止,如果还没有找到,那么这个属性或者方法就是 undefined。
原型链 (prototype chain) 是由每个对象的原型属性所组成的链结构,它用于查找对象属性和方法的过程。如果一个对象不具有某个属性或方法,那么 JavaScript 就会沿着原型链向上查找,直到找到该属性或方法为止。这样就可以实现属性和方法的继承。
下面是一个简单的例子来说明原型和原型链的概念:
// 定义一个构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 给 Person 的原型对象上添加一个 sayHello 方法
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
// 创建一个 Person 的实例
const person = new Person('Tom', 20);
// 调用实例的 sayHello 方法
person.sayHello(); // 输出:Hello, my name is Tom
// person 对象本身没有 sayHello 方法,因此它会沿着原型链向上查找,
// 在 Person.prototype 上找到了 sayHello 方法,并成功调用了它。
在这个例子中,我们定义了一个构造函数 Person,它有两个属性 name 和 age。然后我们给 Person.prototype 添加了一个 sayHello 方法,最后创建了一个 person 对象,并调用了它的 sayHello 方法。因为 person 对象本身没有 sayHello 方法,所以 JavaScript 就会沿着原型链向上查找,在 Person.prototype 上找到了 sayHello 方法,并成功调用了它。
需要注意的是,虽然我们通常将原型对象称为“原型”,但实际上它并不是创建对象的原型,而是在创建对象时被用作参考的对象。在 JavaScript 中,每个对象都有一个隐式的 proto 属性,它指向了创建它的构造函数的原型对象。因此,可以说 proto 属性是在创建对象时用来建立原型链的。
前言
在ES6之前,并没有引入class,所以创建实例的方法是通过构造函数来创建的。
构造函数
构造函数的目的就是创建自定义类,并且创建这个类的实例。构造函数中拥有了类和实例的概念,并且实例和实例之间相互独立。
构造函数本质上就是个普通函数,不过会有一些特性:
1、构造函数一般首字母大写
2、调用方式不一样,普通函数直接调用,构造函数通过new关键字
3、执行流程不一样,其执行流程如下:
1) 立即在堆内存中创建一个新的对象 2) 将这个对象传递给构造函数的this,所以this指向了这个对象 3) 逐行执行代码,通过this将属性添加到了这个对象上 4) 将这个新对象作为返回值返回去
4、普通函数因为没有返回值,所以打印出来都是undefined,而构造那函数会返回这个新对象,所以会打印出来这个新对象
5、构造函数内部用this来构造属性和方法,this指向的就是new出来的实例
注意:那为什么会需要原型(prototype)这个东西呢?
因为如下一个例子:
function Person(name, color) {
this.name = name;
this.color = color;
this.sayHello = function () {
console.log('hello');
};
}
var person1 = new Person('jack', '白色');
var person2 = new Person('rose', '黑色');
person1.sayHello === person2.sayHello
// false
person1和person2两个实例都具有sayHello方法,而这个方法相当于在每个实例对象上都创建了一次,会造成资源浪费,而且根本没必要。
那为什么新增属性有必要创建,而函数没必要呢?因为属性是根据传入的参数生成的,不一样的参数生成的属性值也不一样,如person1的name为Jack,这个没办法抽象出来,必须要创建。但是函数是一段抽象的逻辑,跟参数又没啥关系,完全应该找个地方抽象出来,让所有实例都共享,不要重复去创建。
因此,想了一个办法,就是JavaScript的原型对象(prototype).
prototype属性的作用
JS继承机制的设计思想就是,原型对象的所有属性和方法,都能被它的实例对象共享。注意,原型对象里面不仅仅有方法,也有属性。想想也对嘛,既然方法可以共享,有些属性通用的也可以一起共享嘛。也就是说,如果你把属性和方法定义到了原型上,那么所有的实例对象都能共享,不仅节省了内存,还体现了实例对象之间的联系。
下面研究一下怎样为对象指定原型。在JS中规定,每个函数都有一个prototype属性,它指向一个对象。注意:prototype是函数才会有的属性!!!
function Person() {
}
// 虽然写在注释里,但是你要注意:
// prototype是函数才会有的属性
Person.prototype.name = 'Kevin';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // Kevin
console.log(person2.name) // Kevin
那么这个函数的prototype到底指向什么呢?是这个Person函数的原型吗?
其实,函数的prototype属性指向了一个对象,这个对象正是调用Person构造函数而创建的实例的原型,也就是例子中person1和person2的原型。
在声明了一个函数后,这个构造函数(就是这个声明了的函数)中会有一个属性prototype,这个属性指向的就是这个构造函数对应的原型对象。
如下图:
举个例子:
function Person(name) {
this.name = name;
}
Person.prototype.color = 'white';
var person1 = new Person('大毛');
var person2 = new Person('二毛');
person1.color // 'white'
person2.color // 'white'
这个代码中,构造函数Person的prototype属性,就是实例对象person1和person2的原型对象。 原型对象上添加了一个color属性,所以实例对象都共享了这个属性。
原型对象上的属性不是实例对象自己的属性,所以如果你修改了原型对象,所有实例对象都会变化。(其实,这个原型对象可以想象成在空中的一个共享对象,所有实例都可以去上面取东西,实例保存的是一个对这个共享对象的指针,这个共享对象一变,所有实例对象都得跟着变。这个共享对象就是原型对象,里面可以保存共享的属性和方法。)
Person.prototype.color = 'yellow';
person1.color // "yellow"
person2.color // "yellow"
原型对象的color一但被修改,所有实例对象的color属性也就跟着变化。这是因为实例对象自己其实没有color属性,这个color取得是原型对象上的color属性的值。也就是说,当实例对象本身没有某个属性和方法的时候,他就去原型对象上找该属性或者方法。这就是原型的特殊之处。
如果实例自身就有这个属性或者方法, 那么他就不会再去原型对象上找这个属性或方法了。
function Person(name) {
this.name = name;
}
var person1 = new Person('大毛');
var person2 = new Person('二毛');
person1.color = 'black';
Person.prototype.color = 'yellow';
person1.color // 'black'
person2.color // 'yellow'
总结一下:原型对象的作用,就是定义一个所有实例对象都能共享的属性或方法。这也是它被称作叫原型对象的原因,而实例对象可以看做是原型对象衍生出来的子对象。
对于普通函数来说(也就是不通过new),这个对象没啥用。但是,对于构造函数来说,生成实例的时候,该对象会自动成为实例对象的原型。
那么,怎么去表示实例与实例原型的关系呢?也就是例子中person和Person.prototype之间的关系呢?这时候就用到了 __proto__
隐式原型了。
记住!!!__proto__
是每个JS对象(除了null)上都具有的一个属性,对象才有!!!这个属性会指向该对象的原型对象。
function Person() {
}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true
注意:person跟Person没任何关系!!!person跟Person.prototype才有关系!!!
话说到这里,那么实例对象和构造函数都能指向原型,那么原型是否有属性可以指向构造函数或者实例呢?也就是图上的箭头反过来?
constructor
原型指向实例是没有的,也没办法指过去,因为可以new出来那么多实例,指不过来啊。但是,指向构造函数的倒是有的。这里就用到了第三个属性:constructor。每个原型都有一个constructor指向关联的构造函数。
function Person() {
}
console.log(Person === Person.prototype.constructor); // true
再通俗一点,也就是这个云上共享的对象(原型对象),他有一个构造函数,指向的是包含它的构造函数。毕竟这个云上共享对象(原型对象)是挂在人家构造函数的属性上的,借了人家的宝地。
function Person() {
}
var person = new Person();
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
现在就理清楚原型、实例和构造函数的关系了。
原型链
JS规定,每个对象都有自己的原型对象。一方面,任何一个对象都可以充当其他对象的原型(只要我这个对象保存的属性和方法想被其他对象共享);另一方面,由于原型对象也是对象,他也有自己的原型。因此,这就形成了一个链条,叫 原型链(prototype chain)。对象到原型,再到原型的原型。。。
如果一直往上找,所有对象的原型最终都可以追溯到 Object.prototype, 也就是Object这个构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype里面共享的属性和方法。这样就可以理解来为什么所有对象都有valueOf和toString方法, 因为最上面它们的老祖宗提前开辟了一片地方,把这些可以共享的属性和方法放进来Object.prototype属性上。
那么,Object.prototype也是个对象,他有没有原型呢?有,但是是null,毕竟自己就是元始天尊了。null没有属性和方法,也没有自己的原型(因为都不是对象了),至此,原型链的尽头就是null了。
Object.getPrototypeOf(Object.prototype) //null
当我们读取一个对象的属性时,JS会先从对象本身的属性查找,找不到就通过 __proto__
去它的原型对象看有没有共享的属性,还没有就去原型对象的原型对象上找,直到找到最顶层的Object.prototype
上,还没有就返回undefined了。
如果一个对象自身的属性上定义了一个和原型对象上同名的属性或者方法,那么就优先使用自身上面的属性和方法,也就是 “覆写”
注意,一级级向上寻找某个属性对性能是有影响的。越往上,相当于找的范围越大,链条越长,性能影响也越大。
举例来说,如果让构造函数的prototype属性指向一个数组,就意味着实例对象可以调用数组方法。
var MyArray = function () {};
MyArray.prototype = new Array(); // new array之后返回一个数组对象
var mine = new MyArray();
mine.push(1, 2, 3);
mine instanceof Array // true
补充:
最后,补充三点大家可能不会注意的地方:
constructor
首先是 constructor 属性,我们看个例子:
function Person() {
}
var person = new Person();
console.log(person.constructor === Person); // true
当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:
person.constructor === Person.prototype.constructor
__proto__
其次是 __proto__
,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype
中,实际上,它是来自于 Object.prototype
,与其说是一个属性,不如说是一个 getter/setter
,当使用 obj.__proto__
时,可以理解成返回了Object.getPrototypeOf(obj)
。
真的是继承吗?
最后是关于继承,前面我们讲到“每一个对象都会从原型 '继承' 属性”,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是:
继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。
完结。第一次真正彻底搞懂原型链,牛逼了。
最核心的点:原型对象就是用来存放想要共享给实例的属性和方法的一个对象。!!!
链接:https://www.jianshu.com/p/30a1a94d3fc8
链接:https://github.com/mqyqingfeng/Blog/issues/2
著作权归原作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。