Раньше я думал, что уже хорошо знаю стрелочные функции, и обмануться снова было невозможно. Но несколько дней назад я столкнулся с очень странной проблемой, после долгих мучений я обнаружил, что это ямка, вызванная стрелочными функциями. Отсюда и эта статья~
описание проблемы
Например, у меня есть базовый класс Animal, в котором есть базовый метод sayName. После этого каждый подкласс, который наследуется от него, должен реализовать метод sayName, чтобы подтвердить свою идентичность. Реализация кода базового класса проста:
class Animal {
sayName = () => {
throw new Error('你应该自己实现这个方法');
}
}
Итак, теперь мне нужно наследоваться от базового класса Animal, чтобы реализовать подкласс Pig, и реализация тоже очень проста:
class Pig extends Animal {
sayName() {
console.log('I am a Pig');
}
}
Э, это так просто? Где яма? Однако, когда вы на самом деле запустите его, вы обнаружите, что результат не такой, как ожидалось:
Эх, зачем это. В чем проблема? Почему эта короткая строка кода может сообщить об ошибке?
проблема найдена
После подбрасывания я наконец обнаружил, что это яма стрелочной функции. Нам просто нужно изменить sayName базового класса Animal на обычную функцию или sayName подкласса Pig на функцию стрелки, чтобы решить эту проблему. Итак, что, черт возьми, происходит со стрелочными функциями?
Написав сюда, я вдруг вспомнил, что на этот вопрос меня брал интервьюер! В это время интервьюер спросил о классе,В чем разница между функцией стрелки и обычной функцией класса и функцией связывания в конструкторе. В то время ответ был однозначным, но в случае наследства он перевернул воду. Итак, чтобы ответить на поставленный выше вопрос, давайте сначала ответим на вопрос интервью.
В чем разница между функцией стрелки и обычной функцией класса и функцией связывания в конструкторе
Чтобы увидеть эту проблему более интуитивно, мы можем использоватьbabelРезультаты компиляции кода, чтобы лучше увидеть разницу.
Сначала давайте введем простой код
class A {
constructor() {
this.b = this.b.bind(this);
}
a() {
console.log('a');
}
b() {
console.log('b')
}
c = () => {
console.log('c')
}
}
Давайте посмотримbabelВо что он компилируется:
"use strict";
function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return !!right[Symbol.hasInstance](left); } else { return left instanceof right; } }
function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
var A = /*#__PURE__*/function () {
function A() {
_classCallCheck(this, A);
_defineProperty(this, "c", function () {
console.log('c');
});
this.b = this.b.bind(this);
}
_createClass(A, [{
key: "a",
value: function a() {
console.log('a');
}
}, {
key: "b",
value: function b() {
console.log('b');
}
}]);
return A;
}();
Большая часть скомпилированного кода — это вспомогательные функции, мы можем посмотреть только часть ключевых моментов:
var A = /*#__PURE__*/function () {
function A() {
_classCallCheck(this, A);
_defineProperty(this, "c", function () {
console.log('c');
});
this.b = this.b.bind(this);
}
_createClass(A, [{
key: "a",
value: function a() {
console.log('a');
}
}, {
key: "b",
value: function b() {
console.log('b');
}
}]);
return A;
}();
Из скомпилированных результатов мы можем увидеть разницу между собой:
- Обычная функция: После того, как babel скомпилирован, он будет помещен в прототип функции.
- Функция привязки в конструкторе: после компиляции она будет не только помещена в прототип функции, но и при каждом ее инстанцировании будет генерироваться переменная, привязанная к контексту текущего экземпляра (this.b = this. б.связать(это) ).
- Стрелочная функция: после компиляции babel каждый раз при создании экземпляра будет вызываться defineProperty для привязки содержимого стрелочной функции к текущему контексту экземпляра.
Судя по скомпилированным результатам, для реальной разработки, если вам нужно привязать контекст, лучше всего использовать стрелочные функции. Потому что, если вы используете метод привязки, будет сгенерирована не только функция-прототип, но и каждое создание экземпляра будет генерировать дополнительную функцию.
возобновить
Прочитав комментарии Ю Тэнцзин, я узнал кое-что более важное.
class Для методов и переменных, объявленных со знаком =, они будут использоваться как атрибуты экземпляра, а для атрибутов, объявленных со знаком =, они будут помещены в цепочку прототипов. Например
class A {
a() {
}
b = 2;
c = () => {
}
}
Для этого класса при создании экземпляра b, c будут использоваться как атрибуты экземпляра, а a помещается в цепочку прототипов.
Так почему же это происходит? На самом деле мы видим, что это упоминается в спецификации tc39:Field declarations
В случае прямого написания объявления знака равенства, это фактически синтаксис объявлений полей, который эквивалентен прямому объявлению такого свойства экземпляра.
вернуться к теме
После того, как мы решили предыдущую задачу, вернемся к теме. После понимания результата компиляции стрелочной функции класса в реальной ситуации компиляции на самом деле легче понять нашу проблему.
Q: Почему возникает проблема с выполнением, когда подкласс объявляет sayName как обычную функцию?
A: Если подкласс объявляет sayName как обычную функцию, то sayName, объявленное подклассом, будет помещено в прототип конструктора. Однако, поскольку sayName базового класса использует стрелочные функции, каждый экземпляр будет напрямую иметь переменную sayName. Согласно правилам доступа к переменным javascript, сначала будет искаться сама переменная, а затем она будет искаться в цепочке прототипов, когда она не будет найдена. Поэтому при поиске sayName он напрямую находит функцию sayName, объявленную базовым классом, и ее уже не найти в цепочке прототипов, так что возникает проблема.
Q: Почему подкласс объявляет sayName как стрелочную функцию, и нет никаких проблем с выполнением.
О: При инициализации класса es6 сначала будет выполняться конструктор базового класса, а затем его собственный конструктор. Таким образом, после инициализации базового класса стрелочная функция sayName, объявленная подклассом, переопределяет функцию базового класса, поэтому при выполнении не возникает проблем.
Суммировать
Когда-то я думал, что очень хорошо знаю функции стрелок, но я не ожидал, что меня забьют, и мне до сих пор не от конца учиться! Но у меня также есть более глубокое понимание стрелочных функций внутри класса.
Но после напоминаний от больших парней в области комментариев я обнаружил, что проблема не в стрелочных функциях, переменные, объявленные со знаком = в классе, относятся к синтаксису объявлений полей. Для переменных, объявленных таким образом, они фактически монтируются непосредственно в свойства экземпляра, а не в цепочку прототипов.
Адрес этой статьи находится по адресу ->Адрес моего блога, добро пожаловать, чтобы начать или подписаться