您现在的位置是:网站首页> 编程资料编程资料
一文彻底理解JavaScript原型与原型链_javascript技巧_
2023-05-24
803人已围观
简介 一文彻底理解JavaScript原型与原型链_javascript技巧_
前言
JavaScript中有许多内置对象,如:Object, Math, Date等。我们通常会这样使用它们:
// 创建一个JavaScript Date实例 const date = new Date(); // 调用getFullYear方法,返回日期对象对应的年份 date.getFullYear(); // 调用Date的now方法 // 返回自1970-1-1 00:00:00 UTC(世界标准时间)至今所经过的毫秒数 Date.now()
当然,我们也可以自己创建自定义对象:
function Person() { this.name = '张三'; this.age = 18; } Person.prototype.say = function() { console.log('say'); } const person = new Person(); person.name; // 张三 person.say(); // say看到这些代码,不知道你是否有这些疑问:
new关键执行函数和普通函数执行有什么区别吗?- 对象的实例为什么可以调用构造函数的原型方法,它们之间有什么关系吗?
接下来,让我们带着这些问题一步步深入学习。
new对函数做了什么?
当我们使用new关键字执行一个函数时,除了具有函数直接执行的所有特性之外,new还帮我们做了如下的事情:
- 创建一个空的简单
JavaScript对象(即{}) - 将空对象的
__proto__连接到(赋值为)该函数的prototype - 将函数的
this指向新创建的对象 - 函数中如果没有返回对象的话,将
this作为返回值
用代码表示大概是这样:
// 1. 创建空的简单js对象 const plainObject = {}; // 2. 将空对象的__proto__连接到该函数的prototype plainObject.__proto__ = function.prototype; // 3. 将函数的this指向新创建的对象 this = plainObject; // 4. 返回this return this可以看到,当我们使用new执行函数的时候,new会帮我们在函数内部加工this,最终将this作为实例返回给我们,可以方便我们调用其中的属性和方法。
下面,我们尝试实现一下new:
function _new (Constructor, ...args) { // const plainObject = {}; // plainObject.__proto__ = constructor.prototype; // __proto__在有些浏览器中不支持,而且JavaScript也不推荐直接使用该属性 // Object.create: 创建一个新对象,使用现有的对象提供新创建的对象的__proto__ const plainObject = Object.create(Constructor.prototype); // 将this指向新创建的对象 const result = Constructor.call(plainObject, ...args); const isObject = result !== null && typeof result === 'object' || typeof result === 'function'; // 如果返回值不是对象的话,返回this(这里是plainObject) return isObject ? result : plainObject; }简单用一下我们实现的_new方法:
function Animal (name) { this.name = name; this.age = 2; } Animal.prototype.say = function () { console.log('say'); }; const animal = new Animal('Panda'); console.log(animal.name); // Panda animal.say(); // say在介绍new的时候,我们提到了prototype,__proto__这些属性。你可能会疑惑这些属性的具体用途,别急,我们马上进行介绍!
原型和原型链
在学习原型和原型链之前,我们需要首先掌握以下三个属性:
prototype: 每一个函数都有一个特殊的属性,叫做原型(prototype)constructor: 相比于普通对象的属性,prototype属性本身会有一个属性constructor,该属性的值为prototype所在的函数__proto__: 每一个对象都有一个__proto__属性,该属性指向对象(实例)所属构造函数(类)的原型prototype
以上的解释只针对于JavaScript语言
我们再来看下边的一个例子:
function Fn () { this.x = 100; this.y = 200; this.getX = function () { console.log(this.x); }; } Fn.prototype.getX = function () { console.log(this.x); }; Fn.prototype.getY = function () { console.log(this.y); }; const fn = new Fn()我们画图来描述一下上边代码中实例、构造函数、以及prototype和__proto__之间的关系:

我们再来看一下Function和Object以及其原型之间的关系:

由于Function和Object都是函数,因此它们的所属类为Function,它们的__proto__都指向Function.prototype。而Function.prototype.__proto__又指向Object.prototype,所以它们既可以调用函数原型上的方法,也可以调用对象原型上的方法。
当我们需要获取实例上的某个属性时:
上例中:
- 实例:
fn - 实例所属类:
Fn
- 首先会从自身的私有属性上进行查找
- 如果没有找到,会到自身的
__proto__上进行查找,而实例的__proto__指向其所属类的prototype,即会在类的prototype上进行查找 - 如果还没有找到,继续到类的
prototype的__proto__中查找,即Object.prototype - 如果在
Object.prototype中依旧没有找到,那么返回null
上述查找过程便形成了JavaScript中的原型链。
在理解了原型链和原型的指向关系后,我们看看以下代码会输出什么:
const f1 = new Fn(); const f2 = new Fn(); console.log(f1.getX === f2.getX); console.log(f1.getY === f2.getY); console.log(f1.__proto__.getY === Fn.prototype.getY); console.log(f1.__proto__.getX === f2.getX); console.log(f1.getX === Fn.prototype.getX); console.log(f1.constructor); console.log(Fn.prototype.__proto__.constructor); f1.getX(); f1.__proto__.getX(); f2.getY(); Fn.prototype.getY(); // false // true // true // false // false // Fn // Object // 100 // undefined // 200 // undefined
到这里,我们已经初步理解了原型和原型链的一些相关概念,下面让我们通过一些实际例子来应用一下吧!
借用原型方法
在JavaScript中,我们可以通过call/bind/apply来更改函数中this指向,原型上方法的this也可以通过这些api来进行更改。比如我们要将一个伪数组转换为真实数组,可以这样做:
function fn() { return Array.prototype.slice.call(arguments) } fn(1,2,3) // [ 1, 2, 3]这里我们使用arguments调用了数组原型上的slice,这是怎么做到的呢?我们先简单模拟下slice方法的实现:
arguments是一个类似数组的对象,有length属性和从零开始的索引,它可以调用Object.prototype上的方法,但是不能调用Array.prototype上的方法。
Array.prototype.mySlice = function (start = 0, end = this.length) { const array = []; // 一般会通过Array的实例(数组)调用该方法,所以this指向调用该方法的数组 // 这里我们将this指向了arguments = {0: 1, 1: 2, 2: 3, length: 3} for (let i = 0; i < end; i++) { array[i] = this[i]; } return array; }; function fn () { return Array.prototype.mySlice.call(arguments); } console.log(fn(1, 2, 3)); // [1, 2, 3]可能你想直接调用arguments.slice()方法,但是遗憾的是arguments是一个对象,不能调用数组原型上的方法。
当我们将Array.prototype.slice方法的this指向arguments对象时,由于arguments拥有索引属性以及length属性,所以可以像数组一样根据length和索引来进行遍历,从而相当于用arguments调用了数组原型上的方法。
下面是另一个借用原型方法常见的例子:
Object.prototype.toString.call([1,2,3]) // [object Array] Object.prototype.toString.call(function() {}) // [object Number]这里将Object.prototype.toString的this由对象(Object的实例)改为了数组(Array的实例)和函数(Function的实例),相当于为数组和函数调用了对象上的toString方法,而不是调用它们自身的toString方法。
通过借用原型方法,我们可以让变量调用自身以及自己原型上没有的方法,增加了代码的灵活性,也避免了一些不必要的重复工作。
实现构造函数之间的继承
通过JavaScript中的原型和原型链,我们可以实现构造函数的继承关系。假设有如下A,B俩个构造函数:
function A () { this.a = 100; } A.prototype.getA = function () { console.log(this.a); }; function B () { this.b = 200; } B.prototype.getB = function () { console.log(this.b); };方案一
这里我们可以让B.prototype成为A的实例,那么B.prototype中就拥有了私有方法a,以及原型对象上的方法B.prototype.__proto__即A.prototype上的方法getA。最后记得要修正B.prototype的constructor属性,因为此时它变成了B.prototype.constructor,也就是B。
function A () { this.a = 100; } A.prototype.getA = function () { console.log(this.a); }; B.prototype = new A(); B.prototype.constructor = B; function B () { this.b = 200; } B.prototype.getB = function () { console.log(this.b); };画图理解一下:

下面我们创建B的实例,看下是否成功继承了A中的属性和方法。
const b = new B(); console.log('b', b.a); b.getA(); console.log('b', b.b); b.getB(); // b 100 // 100 // b 200 // 200方案二
我们也可以通过将父构造函数当做普通函数来执行,并通过call指定this,从而实现实例自身属性的继承,然后再通过Object.create指定子构造函数的原型对象。
function A () { this.a = 100; } A.prototype.getA = function () { console.log(this.a); }; // 继承原型方法 // 创建一个新对象,使用一个已经存在的对象作为新创建对象的原型 B.prototype = Object.create(A.prototype); B.prototype.constructor = B; function B () { // 继承私有方法 A.call(this); // 如果有参数的话可以在这里传入 this.b = 200; } B.prototype.getB = function () { console.log(this.b); };这里我们再次通过画图的形式梳理一下逻辑:

下面我们创建B的实例,看下是否成功继承了A中的属性和方法。
const b = new B(); console.log('b', b.a)
相关内容
- 手写实现Vue计算属性_vue.js_
- ant-design-vue动态表格合并案例_vue.js_
- 一文详解JavaScript 如何将 HTML 转成 Markdown_javascript技巧_
- JS数据分析数据去重及参数序列化示例_JavaScript_
- JS疑惑的数据类型及类型判断方法详解_JavaScript_
- javascript中for...of和for..in循环的区别_javascript技巧_
- uni-app如何读取本地json数据文件并渲染到页面上_javascript技巧_
- Evil.js项目源码解读_JavaScript_
- Vue实现网页转PDF方法流程详解_vue.js_
- vue3 使用setup语法糖实现分类管理功能_vue.js_
点击排行
本栏推荐
