JS基础回归01:new操作符,原型和原型链

本篇介绍 new 操作符的背后原理以及 JS 如何依赖原型形成原型链,完成继承。

new 操作符的本质

new 操作符置于构造函数前面,来创建一个基于该构造函数的实例。其仍属于一种模拟 Java 类行为的写法,但它的本质是基于原型链的继承。

JS 是基于原型的语言,并不具备“类”的概念,ES6 中的 class 属于一种语法糖,能够让开发者更好理解。

这里的构造函数,既可以是 JS 已经内置的函数(String, Boolean, Object等),也可以是我们自己定义的普通函数。我们知道,JS 自身提供了一些内置的构造函数,可以用其创建各类数据类型的实例:

// 每一种数据类型都有对应的内置构造函数
// 注意:ES6 新增的 Symbol 类型不支持 new 新建实例
const str = new String('i am a string');
const num = new Number(123);

我们在实际开发中,常使用字面量形式来定义这些数据类型,两者的本质是类似的(但推荐使用后者):

const str = 'i am a string';
const num = 123;

对于自定义的普通函数,仍然可以通过 new 操作符创建其实例:

function Person(name) {
  this.name = name;
  this.sayName = function () {
    console.log(this.name)
  };
}

const personA = new Person('Jack');
personA.sayName(); // 'Jack'

如同内置函数的写法,当一个普通函数作为构造函数时,其首字母需要大写,这只是一种写法上的约定,就算你使用小写,也没错,但不推荐这么做。

如上所述,new 操作符的本质,仍属于基于原型的继承行为。新建的实例拥有其构造函数原型上的所有属性和方法。下面我们具体分析 new 操作符背后发生了什么,方便更好理解其本质。

new 操作符背后发生了什么?

我们提到,new 操作符是在背后默默地为我们完成了一些操作,才能实现实例完整继承构造函数的效果。new 的背后其实是以下的四步操作:

  1. 创建一个空的 JavaScript 对象:{}
  2. 链接该对象和构造函数,也就是设置其原型
  3. 将步骤 1 的对象作为this的上下文
  4. 如果该构造函数没有返回对象,则返回 this

详细来看,第1步很好理解,我们来看第2步是如何将空对象链接到该构造函数的?

其实际的操作仍是基于原型:将空对象的 proto 属性指向构造函数的 prototype 属性,{}.__proto__ === Constructor.prototype

我们可以通过前面的例子进行测试:

personA.__proto__ === Person.prototype // true

我们暂且不纠结 proto 和 prototype 这两个属性,留待后面细解,你可以将它理解为两个插口,两个没有关系的对象,因为它们相爱走到了一起。

完成连接后,这个空对象已经具备了构造函数的全部属性和方法。

接下来要做的是,将该对象作为 this 的上下文,这样我们就可以通过 this 来访问该对象的所有属性和方法。

最后一步,如果构造函数明确返回了一个对象,则我们的实例目前能访问到的属性和方法来自于该对象。

function Person(name) {
  this.name = name;
  this.sayName = function () {
    console.log(this.name)
  };

  // 返回一个对象
  return {
    name: 'Rose'
  }
}

const personA = new Person('Jack');
personA.name; // 'Rose'

如果没有返回任何值,则会返回 this.

若是返回一个原始类型的值,实例会忽视它,仍然拿到this.

function Person(name) {
  this.name = name;
  this.sayName = function () {
    console.log(this.name)
  };

  return 'my name is Bob';
}

const personA = new Person('Jack');
console.log(personA)

现在我们对于 new 的背后发生了什么,已经很清楚,就是新建一个对象,将该对象通过原型与构造函数相连,拥有构造函数返回(this 或者 显示返回的对象)的全部方法和属性。

构造函数与普通函数的区别是:

  1. 前者首字母大写,但不是必须
  2. 普通函数前面加上 new,就是构造函数,会返回一个创建的对象,去掉 new,就是普通函数,会得到其 return 的值。

我们也许会对上面第二步的操作感到疑惑,__proto__prototype的区别和联系是什么?原型链又是怎么实现的?

原型、原型链及继承

首先,继承很好理解,许多语言都有这个功能,其基本的目的是,完成功能的复用。一般来讲,继承指的是面向对象的继承,在 Java 中,通过类实现继承,但在 JS 中,是没有类这个概念的,它拥有一套独立而强大的继承机制:基于原型链的继承,原型链又是基于原型这个特性实现的。

proto、prototype 和 constructor

我们先来理清这三个概念。

  • __proto__:每一个对象都拥有一个隐式的属性__proto__,指向其构造函数的原型对象
  • prototype只有函数才会拥有的属性,指向函数的原型对象
  • constructor: 每一个原型对象都拥有这个属性,指向该对象的构造函数。

首先明确以下事实:

  1. JS 中的所有对象一定都有一个原型,并且继承了来自原型的所有属性和方法,而对象找到这个原型的路径就是 obj.__proto__
  2. 不是所有的对象都会有 prototype 属性,只有函数才有:{x: 1}.prototype 的值就为 undefined.

有点绕,请仔细看看这张经典的图:

image

我们跟着这张图和上面三句话的指引,来看看下面的简单例子:

function Person(name) {
  this.name = name;
}

// sayName方法属于 Person 这个构造函数的原型对象
Person.prototype.sayName = function () {
  return `Hello, I am ${this.name}`;
}

const p1 = new Person('Alice')

console.log(p1.__proto__ === Person.prototype) // true
console.log(Person.prototype.constructor === Person) // true
console.log(p1.name) // Alice
console.log(p1.sayName()) // Hello, I am Alice

从这个简单例子中,我们可以看到,p1既拥有了 Person 的属性,也拥有了 Person 原型对象的方法。这样,三者就完成了一次继承,而这个方式,就是通过原型链实现。

这条链从下游到上游依次是:p1 → Person → Person.prototype.(实际上,这个链条上游更长,Person.prototype仍然拥有自己的原型,一直到 Object.prototype)

所以,我们的 new 操作符仍然是一种继承行为,但其仍属于打造原型链的过程。

在这条链上面,上游的方法和属性被下游的实例所共有,同时,下游的对象可以自由定制自己的属性和方法,当上下游拥有同名的属性和方法时,就会出现“属性遮蔽”的情况:

function Person(name) {
  this.name = name;
  this.sayName = function () {
    return 'Hahaha, I am Bob.';
  }
}

// sayName方法属于 Person 这个构造函数的原型对象
Person.prototype.sayName = function () {
  return `Hello, I am ${this.name}`;
}

const p1 = new Person('Alice')
console.log(p1.sayName()) // "Hahaha, I am Bob."

那么,为什么会出现“属性遮蔽”的行为,这涉及到原型链的工作方式。

我们提到,可以把原型链比作一个上下游的关系,这个上游可达对象的基本构造函数 Object 的原型对象:Object.prototype,下游可以以多种方式进行拓展,new 操作符正是其中一种。

当我们访问一个下游节点的属性时,首先会优先从当前节点开始查询,在上面的例子中,p1 本身没有一个 sayName 方法,所以,它会沿着原型链,找到它的构造函数 Person。

Person 内部定义了 sayName 方法,所有就返回了。如果这里也没有找到,就会继续向上查找,找到其原型对象,也就是 Person.prototype,仍然未找到,继续向上查找,一直到最后的 Object.prototype.这个对象是 null,所以到此为止。

也就是说,Object.prototype 是对象原型链的最上游,发源地,下游的实例从这里继承了 Object 的所有实例和方法,例如 toStinghasOwnProperty,感兴趣的同学可以在控制台打印看看。

我们可以看到,正是通过 __proto__ 以及 prototype 这两个属性通力合作,JS 才能实现继承,打造原型链。

instanceof 操作符的工作机制

看看 MDN 上对于 instanceof 的定义:

The instanceof operator tests whether the prototype property of a constructor appears anywhere in the prototype chain of an object.
instanceof 操作符检测构造函数的 prototype 属性出否出现在一个对象原型链的任何位置。

换句话说:检测一个对象的原型是否出现在另一个对象的原型链上游。按前面的例子进行举例:

console.log(p1 instanceof Person) // true
console.log(p1 instanceof Object) // true
console.log(Person instanceof Object) // true

那么,可以思考,instanceof 是如何工作的呢?

沿着左边对象的原型链向上查询,一直到最顶部,能找到右边对象,返回 true,反之返回 false

也就是判断 left.__proto__ === right.prototype,如果 false,沿着原型链,继续判断:

left.__proto__.__proto__ === right,一直到 Object.prototype.

动手实现一个 new 操作符

我们先回顾 new 操作符背后做的工作:

  1. 创建一个空的 JavaScript 对象:{}
  2. 链接该对象和构造函数,也就是设置其原型
  3. 将步骤 1 的对象作为this的上下文
  4. 如果该构造函数没有返回对象,则返回 this

明确了它背后发生的事情,现在我们动手亲自实现一个 new:

function anotherNew(constructor) {
  // 判断传入的值是否为构造函数
  if (typeof constructor !== 'function') {
    return `${constructor} is not a constructor`;
  }

  let obj = {}; // 1.新建一个空对象
  obj.__proto__ = constructor.prototype;
  this = obj
}

参考文章

1、https://github.com/creeperyang/blog/issues/9
2、https://juejin.im/post/584e1ac50ce463005c618ca2
3、https://juejin.im/post/5c7b963ae51d453eb173896e
4、https://juejin.im/post/58f94c9bb123db411953691b
5、https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容