前言
前几天参加了一场远程面试,面试的前期都很顺利,2面试官说的问题基本上都答的七七八八。
后面陷入了一种尴尬的境地,他们不断翻我的简历,然后皱眉提出了一个问题:我看你简历上有写,熟悉原生JavaScript及常见的设计模式,那你来简单实现一下
new操作符
我当场不知所措,之前只背过
new操作符的原理,手写就GG了,正经人谁手写这个啊?
于是面试后开始慢慢琢磨手写
new操作符是个什么鬼,从头到尾又啃了一遍JavaScript的原型链继承
说起new操作符,就不得不提JavaScript的原型链和继承,我在2019年费了很大的劲去学这个东西,笔记也做了不少
最后也按照教程徒手画出来了完整的原型链结构图图:
这样的:😀
还有这样的:😃
不出意外,过了2年了,再回头看全忘了,这是什么东西,乱七八糟的
因为网上大多数教程都告诉我:原型它是这样的,它该这样,但几乎没有解释清楚它为什么是这样的?内部的原理是什么? 这也是我这么多年学了又忘,忘了又学,但还是根本学不会的根本原因。
原型链继承的定义
我们在网络上到处都读到 Javascript 具有原型继承的文章和教程。 然而,默认情况下,Javascript 仅通过
new运算符提供原型继承。 因此,大多数解释读起来确实令人困惑。 本文旨在阐明什么是原型继承以及如何在 Javascript 上真正使用它。
当你在书上看到 Javascript 原型继承时,经常会看到这样的定义:
“ 当访问对象的属性时,如果对象没有该属性,JavaScript 会向上遍历原型链,直到找到该属性 ”。
Javascript一般使用 proto 属性来表示原型链中的下一个对象。 下面将会详细说一下 proto 和原型之间的区别。
注意:proto 是不遵循ECMA标准的,不要在代码里使用。文中使用它来解释 Javascript 继承的工作原理。
下面伪代码展示了Javascript是如何根据原型链向上查找属性的:
function getProperty(obj, prop) {
if (obj.hasOwnProperty(prop)) //如果自身有指定的属性直接返回
return obj[prop]
else if (obj.__proto__ !== null){ //如果没有,顺着原型链向上查找
return getProperty(obj.__proto__, prop)
}
else{
return undefined
}
}
我们用例子演示一下原型链继承: 一个Point对象有两个坐标属性 x、y 和一个方法 print。
按照之前写的原型链的定义,我们创建一个新对象p,来继承Point对象的三个属性
var Point = {
x: 10,
y: 20,
print: function () {
console.log(this.x, this.y);
}
};
var p = {__proto__: Point}
p.print(); // 10 20
console.log(p.x, p.y) // 10 20
__proto__: Point
注意上面的关键代码__proto__: Point,这个赋值的意思是:将p的__proto__指向Point
这样一来,p就会沿着原型链去Point寻找x,y和print属性,这就是原型链继承
容易误导人的原型链继承
令人困惑的是,很多文章都用__proto__这个定义教Javascript原型继承,但没有给出这个代码详细的解释。 相反,他们给出了这样的东西:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype = {
print: function () {
console.log(this.x, this.y);
}
};
var p = new Point(10, 20);
p.print(); // 10 20
这是什么东西?这与上面给出的代码完全不同。
Point 现在是一个函数,我们使用一个prototype 属性还有new 运算符。 这又是什么?怎么就完成了继承了?
这也是文章开头讲的面试盲点:new操作符工作的原理是什么
实现new操作符的过程
Javascript之父布莱登·艾克。在设计Javascript时,为了让它看起来足够像传统的面向对象的编程语言,比如C++和Java,使用new操作符来创建一个类的实例。所以它在Javascript中添加了new操作符
new 操作符采用函数 F 和参数:new F(arguments...)进行实例化,其中包括下面3个步骤:
- 1. 创建类的实例。 它是一个空对象,其
__proto__属性设置为F.prototype。 - 2. 初始化实例。 使用传递的参数调用函数F,并将F的
this设置为实例。 - 3. 返回实例。
既然了解了new操作符实例化的步骤,我们可以用代码来实现其过程。(这大概是迄今为止最简明易懂的解释了):
//实现new操作符的过程
function New (f) {
/*1. 创建实例,将其__proto__指向F.prototype*/
var n = { '__proto__': f.prototype };
return function () {
/*2. 使用传递的参数调用函数F,并将F的this设置为实例*/
f.apply(n, arguments);
/*3. 返回实例*/
return n;
};
}
以上就是用代码实现new操作符的过程,来看看它能不能用
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype = {
print: function () { console.log(this.x, this.y); }
};
var p1 = new Point(10, 20);
p1.print(); // 10 20
console.log(p1 instanceof Point); // true
var p2 = New (Point)(10, 20); //使用我们实现的New操作符函数
p2.print(); // 10 20
console.log(p2 instanceof Point); // true
可以看到,也能完美实现继承,new操作符内部也并没有那么复杂
实现真正意义的原型链继承
Javascript 规范只为我们提供了new操作符。 然而, 在Javascript还没有提供Object.create 函数之前,我们可以自定义Object.create 函数,来实现真正意义的原型链继承。
Object.create方法,是通过一个原始对象创建一个新的对象,新对象会继承原始对象的所有属性,这这个方法实现了最纯粹的原型继承,对象的属性来自于另一个对象
Object.create = function (oldObj) {
function F() {}
F.prototype = oldObj;
var newObj = F()
return newObj;
};
上面的代码实现了Object.create方法,虽说看起来很奇怪,但它的原理非常简单。 它只是创建一个新对象,将其原型设置为旧对象,然后返回新对象。
如果使用,__proto__,它可以这样写:
Object.create = function (oldObj) {
var newObj = { '__proto__': oldObj };
return newObj;
};
然后我们就可以实现对象的继承:
var Point = {
x: 0,
y: 0,
print: function () { console.log(this.x, this.y); }
};
var p = Object.create(Point);
p.x = 10;
p.y = 20;
p.print(); // 10 20
仔细看下面的代码,可以发现newObj.__proto__ === F.prototype === oldObj
function F() {}
F.prototype = oldObj;
var newObj = F()
var newObj = { '__proto__': oldObj };
当然,也可以像下面这样定义Object.create方法,使用this关键字来代替旧对象,其原理和使用__proto__一样
Object.prototype.begetObject = function () {
function F() {}
F.prototype = this;
return new F();
};
newObject = oldObject.begetObject();