JavaScript中的原型链-前端学习笔记(二)

132 阅读4分钟

此篇文章为本人学习过程中总结的相关笔记,若有错误或不恰当之处,欢迎指正。

JavaScript是一门面向对象语言,但是它不同于其它面向对象语言,它的面向对象是通过原型链实现的。

prototype

在JavaScript中,每一个函数,都有一个prototype属性,属性值为一个普通的对象,这个prototype对象中有一个默认的属性constructor,指向函数本身。

函数的prototype属性

我们先来看一下JavaScript中基于原型链的继承是怎么工作的

function Klass(x) {
	this.x = x;
}
Klass.prototype.y = 2;
Klass.prototype.z = {a: 1};
console.log(Klass.prototype);
// 这时,Klass.prototype属性是这样的
// {
//  y: 2,
//  z: {a: 1},
//  __proto__: Object.prototype
// }
let klass1 = new Klass(1);
let klass2 = new Klass();
klass1.x;   // 1
klass1.y;   // 2
klass1.z;   // {a: 1}
klass2.x;   // undefined
klass2.y;   // 2
klass2.z;   // {a: 1}

上述代码中,klass1的x属性值为传入的参数1,klass2的x属性因为参数为undefined被赋值为undefined,这个没有问题,但是它们的y属性和z属性是哪里来的呢?是从原型链上找到的,即从Klass.prototype对象上拿到的。下面来验证一下。

klass1.z === klass2.z;  // true
klass1.z === Klass.prototype.z; // true
klass1.z.a; // 3
klass2.z.a; // 3
Klass.prototype.z.a;    // 3
// 说明两个实例的z属性和Klass.prototype.z是同一个引用
klass1.y = 3;
klass1.y;  // 3
klass2.y;  // 2

上面的代码中,为什么klass1和klass2的y属性不相同了呢?我们继续看看现在的klass1和klass2是什么。

console.log(klass1);    // Klass {x: 1, y: 3}
console.log(klass2);    // Klass {x: undefined}

klass1的y属性值为3的原因就在上述代码中了,因为klass1.y = 3;这条赋值语句,动态为klass1对象添加了一个值为3的y属性。当我们通过klass1.y取其y属性时,对象本身存在y属性,就不会继续沿着原型链向上查找了。

此时,klass1的y属性为其自身的y属性,klass2的y属性为其原型链上(即Klass.prototype)的y属性。

__proto__

__proto__为对象的原型,所有的对象均有__proto__属性。注意:这里的__proto__并非是一个标准属性,只是某些浏览器暴露出来的属性。

上文说到,当访问对象的属性时,若对象本身没有这个属性,便会沿着原型链去查找,这个原型链就是通过__proto__链接起来的。实例的__proto__属性指向构造函数的prototype,即实例的原型指向构造函数的prototype属性。

obj.__proto__ === Klass.prototype;  // true

我们来看一个栗子:

function Pet(name, age) {
	this.name = name;
	this.age = age;
}
Pet.prototype = {
	constructor: Pet,
	eat (){
		console.log(`${this.name} is eating.`);
	},
	legs: 4,
};
function Dog(...param) {
	Pet.call(this, ...param);
}
Dog.prototype = Object.create(Pet.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function () {
	console.log(`${this.name} is speaking wang.`);
};
let doge = new Dog('doge');
function Monkey(...param) {
	Pet.call(this, ...param);
}
Monkey.prototype = Object.create(Pet.prototype);
Monkey.prototype.constructor = Monkey;
Monkey.prototype.legs = 2;
let jaxssson = new Monkey('jaxssson');
doge.speak();    // doge is speaking wang.
doge.eat();  // doge is eating.
console.log(doge.legs);  // 4
jaxssson.eat();   // jaxssson is eating.
console.log(jaxssson.legs);   // 2

把上面代码的原型链画出来,大致如下。访问实例属性时,沿原型链查找,直到Object.prototype为止。

原型链图

顺便一提,通过不同方式的创建对象,__proto__的指向会有不同。上面是通过new操作符声明实例,所以实例的__proto__指向构造函数的prototype属性。除此之外,还常有以下两种情况。

// 通过字面量创建
let obj = {};
obj.__proto__ === Object.prototype; // true
// 通过Object.create创建
let obj2 = Object.create(obj);
obj2.__proto__ === obj; // true

第一种通过字面量创建的对象,是Object的一个实例。 第二种通过Object.create创建的对象,会与参数中的对象建立原型链联系,将obj2的__proto__指向obj。

之前见到过一个有趣的现象

Function instanceof Object; // true
Object instanceof Function; // true

关于instanceof,不是很了解的可以去看一下我上一篇文章:传送门

我们先来分析一下出现上面现象的原因:

首先,在JavaScript中,非原始类型皆对象,构造函数也不例外,所以Function instanceof Object返回true是没什么问题的。

Function.prototype.__proto__ === Object.prototype;  // true

然后,Object是一个构造函数,既然它是函数,那它肯定是Function类的实例,所以Object instanceof Object返回true也是合理的。

Object.__proto__ === Function.prototype;    // true

分析起来是可以的,但是总觉得是怪怪的。按照上面的思路,Function是一个构造函数,所有的构造函数又都是Function的实例,那么Function是Function的实例?我们来验证一下。

Function.__proto__ === Function.prototype;  // true
Function instanceof Function;   // true

好吧,确实就是这么回事,感觉有点乱,是时候理一下Function和Object之间的关系了。

原型链