azl397985856 / fe-interview

宇宙最强的前端面试指南 (https://lucifer.ren/fe-interview)
Apache License 2.0
2.83k stars 260 forks source link

【每日一题】- 2020-05-07 - 如何使用ES5模拟箭头函数不能new的特性? #123

Closed azl397985856 closed 4 years ago

azl397985856 commented 4 years ago

箭头函数实际上是不能被new的。如下:

function a() {}
new a // ok
b = () => null
new b // TypeError: b is not a constructor

那么如何使用es5的普通函数模拟这种特性呢?

扩展

suukii commented 4 years ago

ecma spec 是如果没有 [[construct]] 内部方法的话就报错,但从这个方向我无从下手...

想到了一个比较 naive 的实现:因为用 new 调用函数的时候会创建一个空对象并把 this 指向它,所以就判断了下,如果 this 是一个空对象的话就抛错,不然就正常执行。

function createArrowFunction(fn) {
  return function () {
    if (Object.keys(this).length === 0) {
      throw new TypeError(`${fn.name || '(intermediate value)'} is not a constructor`)
    }
    fn()
  }
}

var arrowFn = createArrowFunction(function a () {

})

new arrowFn() // a is not a constructor
arrowFn()
feikerwu commented 4 years ago

在new的时候,this会被执行构造函数的实例,判断this instanceof constructor, 就表示当前函数被new了

function A() {
  if (this instanceof A) {
    throw new Error('A should not be called by new');
  }
  // do sth
}

vue源码的init函数也是类似处理

azl397985856 commented 4 years ago

ecma spec 是如果没有 [[construct]] 内部方法的话就报错,但从这个方向我无从下手...

想到了一个比较 naive 的实现:因为用 new 调用函数的时候会创建一个空对象并把 this 指向它,所以就判断了下,如果 this 是一个空对象的话就抛错,不然就正常执行。

function createArrowFunction(fn) {
  return function () {
    if (Object.keys(this).length === 0) {
      throw new TypeError(`${fn.name || '(intermediate value)'} is not a constructor`)
    }
    fn()
  }
}

var arrowFn = createArrowFunction(function a () {

})

new arrowFn() // a is not a constructor
arrowFn()

这种方式bug太多。 如果我的this 就是一个空对象呢? 或者是一个不包含可枚举说明的对象呢?

azl397985856 commented 4 years ago

在new的时候,this会被执行构造函数的实例,判断this instanceof constructor, 就表示当前函数被new了

function A() {
  if (this instanceof A) {
    throw new Error('A should not be called by new');
  }
  // do sth
}

vue源码的init函数也是类似处理

如果我这么用呢?

function a() {}
a.__proto__ = A.prototype
A.call(a) //  A should not be called by new
azl397985856 commented 4 years ago

不正确的方式

如果你听过说new.target,那么可能会想到用new.target 来判断。

new.target 总是指向被 new 的时候的构造函数。 如果没有被new,会返回undefined。 具体可参考ECMA文档

function a() {
 console.log(new.target)
}
a() // undefined
new a // 函数 a 本身

而正如上面提到的,new.target 是ES6 规范内容。因此上面的方法不行。

Babel

我们平时更多的是使用babel进行转化。我们来看下babel 是怎么做的。

具体来说是一个babel的transform 插件 @babel/plugin-transform-arrow-functions

一般情况下, 该插件会把匿名函数转化为朴素普通函数。eg:

// in:
var a = (b) => b;
// out:
var a = function (b) {
  return b;
};

而实际上这并不满足我们的目标“不能通过new构造”。其实这个插件有一个option 叫做 spec,默认是false,如果设置成true 就会按照ECMA标准进行转化,大概是这样的:

var a = (b) => b;
// out:
var _this = this;
var a = function a(b) {
  babelHelpers.newArrowCheck(this, _this);
  return b;
}.bind(this);

可以看出多了一个 newArrowCheck。通过查阅babel 源码也可以发现这一点.

image

其核心逻辑是在函数内部最前面插入了一个函数执行表达式,类似babelHelpers.newArrowCheck(this, _this);。 这个方法又是在哪里定义的呢?

值得注意的是babel允许你写一个自己的逻辑(不那么通用),这些逻辑可以实现注入到babel/helpers 中,具体代码可以看这里:https://github.com/babel/babel/blob/8aa5e574a0c47bd67614fb3a5042479889fed4fb/packages/babel-helpers/src/helpers.js#L663

而对于通用的逻辑,比如获取父级作用域,一般都封装起来大家公用,比如封装到babel-types。

这里我们不难看出实际上问题的根本在于newArrowCheck的实现,这里我贴下代码:

function _newArrowCheck(innerThis, boundThis) {
    if (innerThis !== boundThis) {
      throw new TypeError("Cannot instantiate an arrow function");
    }
}

这个代码和上面编译的代码结合一下就是:

function _newArrowCheck(innerThis, boundThis) {
    if (innerThis !== boundThis) {
      throw new TypeError("Cannot instantiate an arrow function");
    }
}
var _this = this;
var a = function a(b) {
  _newArrowCheck(this, _this);
  return b;
}.bind(this);

简单来分析下上面的代码。

为什么不一致说明是new呢?实际上这有一个前提就是this除了箭头函数,new 和 强制指定,都是谁调用它,指向谁。 具体内容可以参考 JS 中的 this

比如:

foo.call(bar) // this 指向 bar
bar.foo() // this 指向bar
foo() // this 指向window

可以看出foo的指向如果不是强制指定,那么就是”看它在谁上调用“, 注意这种说法不严谨,不过可以这么理解。

正因为JS是基于词法作用域的, 因此这种方式行得通,我们来看个例子。对于形如 bar.foo() window.foo(),会转化为类似:

var bar = {
  foo() {
    var _this = this;
    var a = function a(b) {
      _newArrowCheck(this, _this);
      return b;
    }.bind(this);
  }
}

扩展

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.