理解 new 运算符

359 阅读7分钟

转自一道面试题引发的思考:理解 new 运算符

转自重学 JS 系列:聊聊 new 操作符

今天看到一道面试题,如下,问: 实例化 Person 过程中,Person返回什么(或者 p 等于什么)?

functionPerson(name) {
    this.name = name
    return name;
}
let p = new Person('Tom');

说实话,第一反应我以为值为 'Tom',等到我把代码丢到控制台一输出,才明白我错了。天呐,new运算符给无视掉了吗??? 撇开 new 的存在,我们修改下代码

functionPerson(name) {
    this.name = name
    return name;
}
let p = Person('Tom');
console.log(p);

很显然,输出的结果是 'Tom', 但是有 new 存在呢?接下去,我们来捋一捋。 首先,我先去 MDN上搜索了 new 的定义

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

emmmm,相当晦涩难懂。

那我们试着写几个栗子看看结果吧

functionPerson1(name) {
  this.name = name;
  // 没有返回值
}

functionPerson2(name) {
  this.name = name;
  return name;
  // 返回非对象
}

functionPerson3(name) {
  this.name = name;
  return { a: 1 };
  // 返回对象
}

functionPerson4(name) {
  this.name = name;
  returnnull;
  // 返回null
}

var p1 = new Person1("aa");
var p2 = new Person2("bb");
var p3 = new Person3("cc");
var p4 = new Person4("dd");

console.log(p1); // Person1 {name: "aa"}
console.log(p2); // Person2 {name: "bb"}
console.log(p3); // {a: 1}
console.log(p4); // Person4 {name: "dd"}

根据上面几个栗子,我们能得出结论:当使用 new 来创建对象||调用构造函数时,如果函数没有返回值|| 返回值是非对象,那么返回的就是构造函数实例后的对象(注意:return null,返回的也是构造函数实例后的对象而非null);如果函数return对象,那么返回这个对象

我们接着看 [MDN] 文档的解释,毕竟光光看这几个demo没有说服力。

一起来理解下 new 到底做了什么工作吧~ 就拿下面这个 demo分析

functionPerson(name) {
  this.name = name;
  return {a: 1}
}
var p = new Person('fe')

当调用 new Person(...)时,会进行以下几步:

  • 首先是 继承自 Person.prototype的新对象会被创建
  • 使用参数 'fe' 调用构造函数 Person, 并将 this 绑定到新创建的对象
  • Person 返回的对象就是 new 表达式的结果 =》 Person 返回的对象是 {a: 1} 所以new 表达式的结果为 {a:1} ; 如果 Person 没有返回值(一般构造函数都不返回值)那么使用步骤1创建的对象,即==》 继承自 Person.prototype 的新对象

貌似照着文档能够些许理解了,倘若模拟实现 new运算符更能深入理解 new 以下是 new 的模拟实现,代码来源 : JavaScript深入之new的模拟实现

functionobjectFactory() {
  var obj = newObject(),
  cons = [].shift.call(arguments)
  obj.__proto__ = cons.prototype
  var ret = cons.apply(obj, arguments)
  returntypeof ret === 'object' ? ret|| obj : obj
}
functionPerson(name) {
  this.name = name;
  return {a: 1}
}
var p = objectFactory(Person, 'fe')

当然了,学习别人的代码不能仅仅只是照搬过来,起码得理解这个代码吧。 使用

  • 首先是创建一个对象
  • cons 是调用 objectFactory 方法的第一个参数,即构造函数; 因为 shift 会改变原数组,所以改变后的 argument 即为调用构造函数的参数 (这里补充说明下: arguments 是一个对应于传递给函数的参数的类数组对象。)
  • obj 的原型指向构造函数, 这样 obj 就能访问到构造函数原型上的属性
  • 将构造函数 consthis 指向 obj,这样 obj 能访问构造函数里的属性
  • 判断返回的值是不是一个对象,如果是对象即返回它(当然这里要处理 return null 的特例,因为历史遗留问题 typeof null === 'object');如果不是对象就返回 obj (注意:这里的 obj 已经不是一个空对象)

如果你耐心看到了这里,那么十分感谢。如文章有错误,望给予指正~

function new2(MyFun, ...list) {
	if(!MyFun)  throw new Error("第一个参数必须是函数");
	let o = Object.create(MyFun.prototype);
	let k = MyFun.call(o, ...list);
	return k&&(typeof k === 'object') ? k : o;
}

function Car(color, name) {
	this.color = color;
	this.name = name;
	return null;
}

Car.prototype.fun=function(){
	console.log(this.color,this.name)
}

var obj=new2(Car,123,'小花');
console.log(obj)
console.log(obj.__proto__===Car.prototype);
console.log(Car.prototype.isPrototypeOf(obj))
console.log(Object.prototype.isPrototypeOf(obj))
console.log(obj instanceof Car)
console.log(obj.constructor===Car);

var obj=new2(null,123,'小花');

转自重学 JS 系列:聊聊 new 操作符

这是重学 JS 系列的第一篇文章,写这个系列的初衷也是为了夯实自己的 JS 基础。既然是重学,肯定不会从零开始介绍一个知识点,如有遇到不会的内容请自行查找资料。

new 的作用

我们先来通过两个例子来了解 new 的作用

functionTest(name) {
  this.name = name
}
Test.prototype.sayName = function () {
    console.log(this.name)
}
const t = new Test('yck')
console.log(t.name) // 'yck'
t.sayName() // 'yck'复制代码

从上面一个例子中我们可以得出这些结论:

  • new 通过构造函数 Test 创建出来的实例可以访问到构造函数中的属性
  • new 通过构造函数 Test 创建出来的实例可以访问到构造函数原型链中的属性,也就是说通过 new 操作符,实例与构造函数通过原型链连接了起来

但是当下的构造函数 Test 并没有显式 return 任何值(默认返回 undefined),如果我们让它返回值会发生什么事情呢?

functionTest(name) {
  this.name = name
  return1
}
const t = new Test('yck')
console.log(t.name) // 'yck'复制代码

虽然上述例子中的构造函数中返回了 1,但是这个返回值并没有任何的用处,得到的结果还是和之前的例子完全一样。

那么通过这个例子,我们又可以得出一个结论:

  • 构造函数如果返回原始值(虽然例子中只有返回了 1,但是你可以试试其他的原始值,结果还是一样的),那么这个返回值毫无意义

试完了返回原始值,我们再来试试返回对象会发生什么事情吧

functionTest(name) {
  this.name = name
  console.log(this) // Test { name: 'yck' }return { age: 26 }
}
const t = new Test('yck')
console.log(t) // { age: 26 }console.log(t.name) // 'undefined'复制代码

通过这个例子我们可以发现,虽然构造函数内部的 this 还是依旧正常工作的,但是当返回值为对象时,这个返回值就会被正常的返回出去。

那么通过这个例子,我们再次得出了一个结论:

  • 构造函数如果返回值为对象,那么这个返回值会被正常使用

这两个例子告诉了我们一点,构造函数尽量不要返回值。因为返回原始值不会生效,返回对象会导致 new 操作符没有作用。

通过以上几个例子,相信大家也大致了解了 new 操作符的作用了,接下来我们就来尝试自己实现 new 操作符。

自己实现 new 操作符

首先我们再来回顾下 new 操作符的几个作用

  • new 操作符会返回一个对象,所以我们需要在内部创建一个对象
  • 这个对象,也就是构造函数中的 this,可以访问到挂载在 this 上的任意属性
  • 这个对象可以访问到构造函数原型上的属性,所以需要将对象与构造函数链接起来
  • 返回原始值需要忽略,返回对象需要正常处理

回顾了这些作用,我们就可以着手来实现功能了

functioncreate(Con, ...args) {
  let obj = {}
  Object.setPrototypeOf(obj, Con.prototype)
  let result = Con.apply(obj, args)
  return result instanceofObject ? result : obj
}
复制代码

这就是一个完整的实现代码,我们通过以下几个步骤实现了它:

  1. 首先函数接受不定量的参数,第一个参数为构造函数,接下来的参数被构造函数使用
  2. 然后内部创建一个空对象 obj
  3. 因为 obj 对象需要访问到构造函数原型链上的属性,所以我们通过 setPrototypeOf 将两者联系起来。这段代码等同于 obj.__proto__ = Con.prototype
  4. obj 绑定到构造函数上,并且传入剩余的参数
  5. 判断构造函数返回值是否为对象,如果为对象就使用构造函数返回的值,否则使用 obj,这样就实现了忽略构造函数返回的原始值

接下来我们来使用下该函数,看看行为是否和 new 操作符一致

functionTest(name, age) {
  this.name = name
  this.age = age
}
Test.prototype.sayName = function () {
    console.log(this.name)
}
const a = create(Test, 'yck', 26)
console.log(a.name) // 'yck'console.log(a.age) // 26
a.sayName() // 'yck'复制代码

虽然实现代码只有寥寥几行,但是结果很完美

最后

我们通过这篇文章重学了 new 操作符,如果你还有什么疑问欢迎在评论区与我互动。

我所有的系列文章都会在我的 Github 中最先更新,有兴趣的可以关注下。今年主要会着重写以下三个专栏

  • 重学 JS
  • React 进阶
  • 重写组件

最后,觉得内容有帮助可以关注下我的公众号 「前端真好玩」咯,会有很多好东西等着你。