龙空技术网

「前端」图解 JS 原型和原型链,一文搞懂!

架构思考 864

前言:

此时朋友们对“js 原型原型链”大致比较注重,小伙伴们都想要学习一些“js 原型原型链”的相关资讯。那么小编也在网摘上收集了一些有关“js 原型原型链””的相关内容,希望各位老铁们能喜欢,兄弟们快快来学习一下吧!

原型

在 JavaScript 中,对象有一个特殊的隐藏属性 [[Prototype]],它要么为 null,要么就是对另一个对象的引用,该对象被称为“原型”。

let animal = { eats: true};console.log(animal);

打印 animal 对象,可以看到隐藏的 [[Prototype]]对象。

打开[[Prototype]]可以看到 animal 对象继承了 Object.prototype的各种属性。

(到现在,你可能不理解什么是Object.prototype,没关系,先继续往下面看)

原型继承

当我们从对象中读取一个缺失的属性时,JavaScript 会自动从原型中获取该属性。 在编程中,这种行为被称为“原型继承”。

animal 对象并没有toString方法。

但它从其原型(即Object.prototype)中继承了该方法。

设置原型

属性 [[Prototype]] 是内部的而且是隐藏的,但是这儿有很多设置它的方式。

法一:

使用特殊的名字 __proto__,在现代JavaScript中,__proto__ 是可读可写的。

let animal = { eats: true }; let rabbit = { jumps: true }; rabbit.__proto__ = animal; // 设置 rabbit.[[Prototype]] = animal

现在,如果我们从 rabbit 中读取一个它没有的属性,JavaScript 会自动从 animal 中获取。

console.log(rabbit.eates); //true

原型可以很长,层层继承就形成了原型链。

let animal = { eats: true, walk(){ console.log('Animal walk'); }};  let rabbit = { __proto__:animal, jumps:true}let longEar = { earLength:10, __proto__:rabbit}// walk()和jumps都是继承而来的longEar.walk(); //Animal Walkconsole.log(longEar.jumps); //true

可能有人要脑洞大开了,让animal再继承LongEar形成闭环会发生什么?

let animal = { eats: true, walk(){ console.log('Animal walk'); }};let rabbit = { __proto__:animal, jumps:true}let longEar = { earLength:10, __proto__:rabbit}animal.__proto__ = longEar;

会报 TypeError。

最后,有4个点要注意:

原型链不能形成闭环。__proto__的值只能是对象或者null,其他的类型都会被忽略。一个对象不能从其他两个对象获得继承。一个对象只能有一个 [[Prototype]]。_proto__ 与内部的 [[Prototype]] 不一样。__proto__ 是 [[Prototype]] 的 getter/setter。法二:

尽管连node也支持__proto__这种写法,但是我们应该使用Object.getPrototypeOf()和Object.setPrototypeOf()来替代。

let animal = {eats: true};console.log(Object.getPrototypeOf(animal) === Object.prototype); //true
let animal = {eats: true};let rabbit = {jumps:true};Object.setPrototypeOf(rabbit,animal); //将animal设置为rabbit的原型console.log(rabbit);
法三:

使用Object.create(proto),用给定的 proto 作为原型创建一个空对象。

let animal = {eats: true};let rabbit = Object.create(animal); //让animal作为rabbit的原型

我们使用对象字面量或者Object构造函数创建的对象会默认继承自Object.prototype。

let obj1 = {}; //对象字面量let obj2 = new Object(); //构造函数

如果我们使用Object.create()创建对象时,且Object.prototype为原型,最终的结果会和上面一样。

let obj3 = Object.create(Object.prototype);console.log(obj3);

不过,如果让它以 null 为原型,就会创建一个真正空空如也的对象。

let obj4 = Object.create(null);console.log(obj4);
函数.prototype

前面一直在说Object.prototype,那么这个到底是个什么玩意儿?

我们知道,可以通过new 构造函数名()来创建一个新的对象。在这里,将构造函数名简写为F,也即 new F()。

其实不仅构造函数,任何函数都有 prototype 属性,即使我们没有提供它。

如果 F.prototype 是一个对象,那么 new 操作符会使用它为新对象设置 [[Prototype]]。

这里的 F.prototype 指的是 F 的一个名为 "prototype" 的常规属性。这听起来与“原型”这个术语很类似,但这里我们实际上指的是具有该名字的常规属性。

let animal = {eats: true};function Rabbit(name){ this.name = name;}// 将Rabbit函数的prototype属性值设置为animal对象Rabbit.prototype = animal;let rabbit = new Rabbit("White Rabbit");// rabbit.__proto__ == animalconsole.log(rabbit.eats); //true

设置 Rabbit.prototype = animal 的字面意思是:“当创建了一个 new Rabbit 时,把它的 [[Prototype]] 赋值为 animal”。

F.prototype 属性仅在 new F 被调用时使用,它为新对象的 [[Prototype]] 赋值。

如果在创建之后,F.prototype 属性有了变化(F.prototype = <another object>),那么通过 new F 创建的新对象也将随之拥有新的对象作为 [[Prototype]],但已经存在的对象将保持旧有的值。

函数默认的prototype

每个函数都有prototype属性,默认的 "prototype" 是一个只有属性 constructor 的对象,属性 constructor 指向函数自身。

我们可以检查一下:

fuction Rabbit(){}console.log(Rabbit.prototype.constuctor === Rabbit) // true

通常,如果我们什么都不做,constructor 属性可以通过 [[Prototype]] 给所有 rabbits 使用:

function Rabbit() {}// by default:// Rabbit.prototype = { constructor: Rabbit } let rabbit = new Rabbit(); // 继承自{constructor: Rabbit} console.log(rabbit.constructor == Rabbit); // true (from prototype)

我们可以使用 constructor 属性来创建一个新对象,该对象使用与现有对象相同的构造器。因为新创建的对象继承的constuctor指向原来的构造函数!

function Rabbit(name) { 	this.name = name; 	alert(name);} let rabbit = new Rabbit("White Rabbit"); let rabbit2 = new rabbit.constructor("Black Rabbit");

但想得到好,……JavaScript 自身并不能确保正确的 "constructor" 函数值。

比如我们如果将函数的prototype属性直接替换掉,那就不会存在constructor属性。

function Rabbit(){}Rabbit.prototype = {eats: true};let rabbit = new Rabbit();console.log(rabbit.constructor === Rabbit); //false

因此,为了确保正确的 "constructor",我们可以选择添加/删除属性到默认 "prototype",而不是将其整个覆盖:

Rabbit.prototype.jumps = true;// 默认的 Rabbit.prototype.constructor 被保留了下来

或者,也可以手动重新创建 constructor 属性:

Rabbit.prototype = { jumps: true, constructor: Rabbit};
Object.prototype
let obj = new Object();//其实和 let obj = {};是一样的

还记得上面的说的:如果 F.prototype 是一个对象,那么 new 操作符会使用它为新对象设置 [[Prototype]]。

Object 就是一个内建的对象构造函数,其自身的 prototype 指向一个带有 toString 和其他方法的一个巨大的对象。上面的创建对象的操作就相当于:

// 创建新对象let obj = new Object();//默认进行了obj.__proto__ = Object.prototype;console.log(obj.__proto__ === Object.prototype); //true

另外 Object.prototype 上方的链中没有更多的 [[Prototype]]。

另外,咱们也不能给 Object.prototype继续设置原型对象了。

其他内建对象继承自Object.prototype

其他内建对象,像 Array、Date、Function 及其他,都在 prototype 上挂载了方法。

例如,当我们创建一个数组 [1, 2, 3],在内部会默认使用 new Array() 构造器。因此 Array.prototype 变成了这个数组的 prototype,并为这个数组提供数组的操作方法。这样内存的存储效率是很高的。

按照规范,所有的内建原型顶端都是 Object.prototype。这就是为什么有人说“一切都从对象继承而来”。

让我们来验证一下:

let arr = [1, 2, 3];arr.__proto__ === Array.prototype; // true//或者说Array.prototype.isPrototypeOf(arr); //trueObject.prototype.isPrototypeOf(arr); //true
总结

前言万语,不如手绘一张图。

构造函数与prototype与[[Prototype]]的关系:

如果说,继续从 那个普通的构造函数出发,一直画原型链:

另外,我们也可以从 F.prototype出发,继续往上画。

而这只是最简单的情况,就已经非常之复杂了。

//验证代码function F() {}const obj = new F();// 验证 obj的直接原型是否为F.prototypeconsole.log(Object.getPrototypeOf(obj) === F.prototype); //true// 验证F.prototype的原型是否为 Object.prototypeconsole.log(Object.getPrototypeOf(F.prototype) === Object.prototype); //true// 验证普通构造函数F的直接原型是否为Function.prototypeconsole.log(Object.getPrototypeOf(F) === Function.prototype); //true// 验证Function.prototype 的原型是否为 Object.prototypeconsole.log(Object.getPrototypeOf(Function.prototype) === Object.prototype); //true

就这样简单的function F() {} const obj = new F();就已经产生超级复杂的原型链了,而这只是最简单的情况。

文章来源:嘉琪coder_

标签: #js 原型原型链 #html原型下载