一文彻底理解JavaScript原型与原型链

目录
  • 前言
  • new对函数做了什么?
  • 原型和原型链
  • 借用原型方法
  • 实现构造函数之间的继承
    • 方案一
    • 方案二
  • class extends实现继承
  • 结语

前言

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__之间的关系:

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

由于FunctionObject都是函数,因此它们的所属类为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.toStringthis由对象(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.prototypeconstructor属性,因为此时它变成了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);
b.getA();
console.log('b', b.b);
b.getB();
// b 100
// 100
// b 200
// 200

class extends实现继承

es6中为开发者提供了extends关键字,可以很方便的实现类之间的继承:

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);
b.getA();
console.log('b', b.b);
b.getB();
// b 100
// 100
// b 200
// 200

大家可能会好奇classextends关键字是如何实现继承的呢?下面我们用babel 编译代码,看下其源码中比较重要的几个点:

看下这俩个方法的实现:

值得留意的一个地方是:extends将父类的静态方法也继承到了子类中

class A {
  constructor () {
    this.a = 100;
  }

  getA () {
    console.log(this.a);
  }
}

A.say = function () {
  console.log('say');
};

class B extends A {
  constructor () {
    // 继承私有方法
    super();
    this.b = 200;
  }

  getB () {
    console.log(this.b);
  }
}
B.say(); // say

extends的实现类似于方案二:

  • apply方法更改父类this指向,继承私有属性
  • Object.create继承原型属性
  • Object.setPrototypeOf继承静态属性

结语

理解JavaScript的原型原型链可能并不会直接提升你的JavaScrit编程能力,但是它可以帮助我们更好的理解JavaScript中一些知识点,想明白一些之前不太理解的东西。在各个流行库或者框架中也有对于原型或原型链的相关应用,学习这些知识也可以为我们阅读框架源码奠定一些基础。

到此这篇关于一文彻底理解JavaScript原型与原型链的文章就介绍到这了,更多相关JS原型与原型链内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • JavaScript原型与原型链深入探究使用方法

    目录 原型(prototype) 显示原型与隐式原型 原型链 原型链属性问题 原型链 instanceof 使用 练习 原型(prototype) 每一个函数都有一个 prototype 属性,它默认指向一个Object空对象(即称为:原型对象). <script> console.log(Date.prototype, typeof Date.prototype); function fun(){ } fun.prototype.test = function(){ //给原型对象添加一个方

  • Javascript 原型与原型链深入详解

    目录 前言 对象 原型 原型链 javascript中的类 new的实现 instanceof的实现 javascript的继承 总结 前言 在前端这块领域,原型与原型链是每一个前端 er 必须掌握的概念.多次在面试或者一些技术博客里面看见这个概念.由此可见,这个玩意对于前端来说有多重要.其实它本身理解起来不难,但是很多刚入行前端的同学,看到prototype.__proto__理解起来还是有点吃力,然后脑子里面就乱成一锅粥,就像我一样. 但是这是很正常的事情,没什么大不了的,就像我们想要学会跑

  • 一文搞懂JavaScript中原型与原型链

    目录 1.构造函数原型prototype 2.对象原型__proto__ 3.constructor构造函数 4.原型链 5.原型对象中的this指向 6.扩展内置对象(原型对象的应用) 在ES6之前,我们面向对象是通过构造函数实现的.我们把对象的公共属性和方法放在构造函数里 像这样: function student(uname,age) { this.uname = uname; this.age = age; this.school = function() { console.log('

  • 三张图带你搞懂JavaScript的原型对象与原型链

    对于新人来说,JavaScript的原型是一个很让人头疼的事情,一来prototype容易与__proto__混淆,二来它们之间的各种指向实在有些复杂,其实市面上已经有非常多的文章在尝试说清楚,有一张所谓很经典的图,上面画了各种线条,一会连接这个一会连接那个,说实话我自己看得就非常头晕,更谈不上完全理解了.所以我自己也想尝试一下,看看能不能把原型中的重要知识点拆分出来,用最简单的图表形式说清楚. 我们知道原型是一个对象,其他对象可以通过它实现属性继承.但是尼玛除了prototype,又有一个__

  • JavaScript三大重点同步异步与作用域和闭包及原型和原型链详解

    目录 1. 同步.异步 2. 作用域.闭包 闭包 作用域 3. 原型.原型链 原型(prototype) 原型链 如图所示,JS的三座大山: 同步.异步 作用域.闭包 原型.原型链 1. 同步.异步 JavaScript执行机制,重点有两点: JavaScript是一门单线程语言 Event Loop(事件循环)是JavaScript的执行机制 JS为什么是单线程 最初设计JS是用来在浏览器验证表单操控DOM元素的是一门脚本语言,如果js是多线程的,那么两个线程同时对一个DOM元素进行了相互冲突

  • JavaScript原型链中函数和对象的理解

    目录 __ proto__ prototype.__ proto__ 理解 __ proto__ 最近在看高程4,原型链肯定是绕不过的,本瓜之前一直认为,只要记住这句话就可以了: 一个对象的隐式原型(__proto__)等于构造这个对象的构造函数的显式原型(prototype) 确实,所有对象都符合这句真理,在控制台打印一试便知: const str = new String("123") str.__proto__ === String.prototype // true const

  • JavaScript原型和原型链与构造函数和实例之间的关系详解

    目录 原型 原型链 原型 如图所示: 1.instanceof检测构造函数与实例的关系: function Person () {.........} person = new Person () res = person instanceof Person res  // true 2.实例继承原型上的定义的属性: function Person () {........} Person.prototype.type = 'object n' person = new Person () re

  • JavaScript原型链及常见的继承方法

    目录 原型链 原型链的概念 原型链的问题 几种常见的继承方法 盗用构造函数 组合继承 原型式继承 寄生式继承 寄生组合式继承 原型链 原型链的概念 在JavaScript中,每一个构造函数都有一个原型,这个原型中有一个属性constructor会再次指回这个构造函数,这个构造函数所创造的实例对象,会有一个指针(也就是我们说的隐式原型__proto__或者是浏览器中显示的[[Prototype]])指向这个构造函数的原型对象.如果说该构造函数的原型对象也是由另外一个构造函数所创造的实例,那么该构造

  • 一文彻底理解JavaScript原型与原型链

    目录 前言 new对函数做了什么? 原型和原型链 借用原型方法 实现构造函数之间的继承 方案一 方案二 class extends实现继承 结语 前言 JavaScript中有许多内置对象,如:Object, Math, Date等.我们通常会这样使用它们: // 创建一个JavaScript Date实例 const date = new Date(); // 调用getFullYear方法,返回日期对象对应的年份 date.getFullYear(); // 调用Date的now方法 //

  • 带你彻底理解JavaScript中的原型对象

    一.什么是原型 原型是Javascript中的继承的基础,JavaScript的继承就是基于原型的继承. 1.1 函数的原型对象 ​ 在JavaScript中,我们创建一个函数A(就是声明一个函数), 那么浏览器就会在内存中创建一个对象B,而且每个函数都默认会有一个属性 prototype 指向了这个对象( 即:prototype的属性的值是这个对象 ).这个对象B就是函数A的原型对象,简称函数的原型.这个原型对象B 默认会有一个属性 constructor 指向了这个函数A ( 意思就是说:c

  • 深入理解JavaScript作用域和作用域链

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理.今天这篇文章对JavaScript作用域和作用域链作简单的介绍,希望能帮助大家更好的学习JavaScript. JavaScript作用域 任何程序设计语言都有作用域的概念,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期.在JavaScript中,变量的作用域有全局作用域和局部作用域两种. 1. 全局作用域(Global Sc

  • 深入理解javascript构造函数和原型对象

    常用的几种对象创建模式 使用new关键字创建 最基础的对象创建方式,无非就是和其他多数语言一样说的一样:没对象,你new一个呀! var gf = new Object(); gf.name = "tangwei"; gf.bar = "c++"; gf.sayWhat = function() { console.log(this.name + "said:love you forever"); } 使用字面量创建 这样似乎妥妥的了,但是宅寂的

  • 理解javascript中的原型和原型链

    原型 大家都知道,JavaScript 不包含传统的类继承模型,而是使用 prototype 原型模型.代码实现大概是这样子的 function Student(name){ this.name = name; } var Kimy = new Student("Kimy"); Student.prototype.say = function(){ console.log(this.name + "say"); } Kimy.say(); //Kimysay Kim

  • 深入理解Javascript中的作用域链和闭包

    首先我们回顾下之前一篇关于介绍数组遍历的文章: 请先看上一篇中提到的for循环代码: var array = []; array.length = 10000000;//(一千万) for(var i=0,length=array.length;i<length;i++){ array[i] = 'hi'; } var t1 = +new Date(); for(var i=0,length=array.length;i<length;i++){ } var t2 = +new Date();

  • JavaScript作用域与作用域链深入解析

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理.今天这篇文章对JavaScript作用域和作用域链作简单的介绍,希望能帮助大家更好的学习JavaScript. JavaScript作用域 任何程序设计语言都有作用域的概念,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期.在JavaScript中,变量的作用域有全局作用域和局部作用域两种. 1. 全局作用域(Global Sc

  • 深入理解JavaScript系列(6) 强大的原型和原型链

    前言 JavaScript 不包含传统的类继承模型,而是使用 prototypal 原型模型. 虽然这经常被当作是 JavaScript 的缺点被提及,其实基于原型的继承模型比传统的类继承还要强大.实现传统的类继承模型是很简单,但是实现 JavaScript 中的原型继承则要困难的多. 由于 JavaScript 是唯一一个被广泛使用的基于原型继承的语言,所以理解两种继承模式的差异是需要一定时间的,今天我们就来了解一下原型和原型链. 原型 10年前,我刚学习JavaScript的时候,一般都是用

  • 深入理解JavaScript编程中的原型概念

    JavaScript 的原型对象总是让人纠结.即使是经验丰富的JavaScript专家甚至其作者,经常对这一概念给出很有限的解释.我相信问题来自于我们对原型最早的认识.原型总是与new, constructor 以及令人困惑的prototype属性紧密联系.事实上,原型是一个相当简单的概念.为了更好地理解它,我们需要忘记我们所'学到'的构造原型,然后,追本溯源. 什么是原型? 原型是一个从其他对象继承属性的对象. 是不是任何对象都可以是原型? 是的 那些对象有原型? 每个对象都有一个默认的原型.

  • JavaScript从原型到原型链深入理解

    构造函数创建对象 我们先使用构造函数创建一个对象: function Person() { } var person = new Person(); person.name = 'Kevin'; console.log(person.name) // Kevin 在这个例子中,Person 就是一个构造函数,我们使用 new 创建了一个实例对象 person. 很简单吧,接下来进入正题: prototype 每个函数都有一个 prototype 属性,就是我们经常在各种例子中看到的那个 prot

随机推荐