JS原型与原型链

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

JS原型与原型链

夏日   2022-09-28 我要评论

前言

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中一些知识点,想明白一些之前不太理解的东西。在各个流行库或者框架中也有对于原型或原型链的相关应用,学习这些知识也可以为我们阅读框架源码奠定一些基础。

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们