全面了解原型、原型链
原型和继承是js中非常重要的两大概念。深入了解原型,也是学好继承的前提。 本文将从 普通对象、引用类型对象、内置对象三个方向全面了解原型和原型链。
普通对象的原型:
我们都知道,访问一个对象实例上的属性和方法时,首先在实例本身查找。
如果没有找到,会到实例对象的隐式属性__proto__上查找,就是该实例的构造函数的原型对象。如果还没有,会继续访问原型对象的原型,直到Object.prototype.__proto__(null)为止。
这就是所有对象都含有toString方法的原因,因为toString是Object.prototype上面的方法。所有对象沿着原型链最后都会找到Object.prototype。
function Person(name) {
this.name = name
}
Person.prototype.getName = function() {
console.log(this.name)
}
let person = new Person('Jack')
//在实例上直接找到了name属性。
console.log(person.name)
//在实例本身没有找到getName方法,在person.__proto__ 上找到了getName方法
person.getName()
//在实例本身没有找到getName方法,在person.__proto__ 上也没有,继续查找person.__proto__.__proto__ 也就是Object.prototype 找到了toString方法
console.log(person.toString())
沿着__proto__查找属性的这一链条,就是我们说的原型链。那么在js中,实例是怎样访问到构造函数原型对象上面的方法?实例又是怎么通过__proto__沿着原型链向上追溯的?
我们知道在js中:
- 每个函数都存在
prototype属性,指向函数的原型对象。js中可以使用构造函数实例化来创建一个对象,构造函数含有prototype属性,会指向构造函数的原型对象。 - 每个对象都存在
__proto__属性,来描述对象的原型。并且对象的原型会指向“实例该对象的构造函数的prototype”,即instance.__proto__ === construction.prototype。 - 函数的原型对象也是对象,所有函数的原型对象的
__proto__会指向Object.prototype
上图说话:
构造函数的原型对象上有一个
constructor属性,会指向构造函数本身。
控制台打印:
console.log(person.__proto__)
console.log(Person.prototype)
console.log(person.__proto__ === Person.prototype)
console.log(Person.prototype.constructor === Person)
引用类型对象的原型:
上面的例子,我们了解了普通对象和构造函数的原型原型链。在js中,万物皆对象,引用类型的值(数组,对象,函数)同样也是对象,是对象就有__proto__属性,会指向对象的原型,对应的构造函数的原型对象。
数组,对象,函数等引用类型对应的构造函数就是js中内置的一些函数Array、Object、Function。可以通过这些构造函数去创建引用类型的值。只是在平时开发中,我们习惯使用字面量的方式声明一个数组或者对象。但是无论用哪种方式去创建一个引用类型,他们的原型链都是一样的。
let arr = new Array(1,2,3)
let obj = new Object({name: 'Jack'})
let fn = new Function('a', 'b', 'return a + b');
alert(fn(1, 2) ); // 3
根据之前说的到普通对象的__proto__属性会指向其构造函数的prototype,我们可以知道,这些引用类型的对象跟其构造函数的关系:
console.log(arr.__proto__ === Array.prototype) //true
console.log(obj.__proto__ === Object.prototype) //true
console.log(fn.__proto__ === Function.prototype) //true
这也是为什么,所有函数都有call、apply、bind方法。因为这些方法是在Function.prototype上面,函数调用call,自身没有找到方法,沿着原型链,访问
fn.__proto__,找到call方法。
Function.prototype.hasOwnProperty('call') // true
内置函数对象的原型:
我们通过数组、对象等引用类型的值,知道了他们的原型和一些内置构造函数的关系。
那么内置的这些函数Object、Function、Number、String、Array、Boolean,他们都是函数,那么同样也是对象,他们的__proto__又指向何处呢?
我们知道,普通函数可以通过new Function()创建,所以普通函数的__proto__指向Function.prototype。那么内置的这些函数同样属于函数,也是通过new Function()创建出来的函数对象,同理,他们的__proto__也指向Function.prototype。
// 下面打印都是true
console.log(Object.__proto__ === Function.prototype)
console.log(Array.__proto__ === Function.prototype)
console.log(Number.__proto__ === Function.prototype)
console.log(String.__proto__ === Function.prototype)
console.log(Boolean.__proto__ === Function.prototype)
console.log(Date.__proto__ === Function.prototype)
不管是普通函数,还是内置的构造函数,他们的__proto__都指向Function.prototype。那Function自己的__proto__指向哪里呢?
Function是所有函数的构造函数,但是Function也是函数,那Function的__proto__应该指向他的构造函数的prototype。然而实际并不是想的那样。
打印发现Function的__proto__ 指向Function的prototype。
这就引发了js中的哲学思考,先有鸡还是先有蛋?或者说是Function就是js中的神,他不仅可以构造出其他函数,同时自己创造了自己?
总结:
- 访问对象的一个属性,先在自身查找,如果没有,会访问对象的
__proto__,沿着原型链查找,一直找到Object.prototype.__proto__。 - 每个函数都有
prototype属性,会指向函数的原型对象。 - 所有函数的原型对象的
__proto__,会指向Object.prototype。 - 每个对象都有
__proto__属性,会指向该对象的构造函数的原型对象。 - 引用类型的值,他们的
__proto__,会指向对应构造函数的prototype。 - 一些内置的构造函数,如
Object、Array、String、Boolean,他们的__proto__属性,指向Function.prototype。 Function.__proto__指向Function.prototype。- 原型链的尽头是
Object.prototype.__proto__,为null。
下面附上网上很全的一张原型链图解。如果这张图看明白了,那就明白了原型原型链,同时知道了js中所有对象的关系。
如果对原型、原型链已经清楚,但是对JS中的继承不太了解的同学,可以查看作者的另一篇文章: # 2022年,在学一遍JS继承、class类,其中对JS常见的继承方式,es6的class继承做了讲解。