Дисклеймер: так как вопрос скорее теоретический, я не стараюсь предоставить "адекватное" решение.
Конечно, все это нельзя использовать в реальном проекте!
Интересный вопрос.
Начнем с того, что определимся насколько критично сохранить сигнатуру функции? В зависимости от этого, решения можно поделить на 2 категории:
- где допускается изменение сигнатуры
- где сигнатура должна быть строго такой и никакой иначе
Также есть ряд решений, подразумевающий предобработку кода тем или иным путем.
Стоит оговориться, что строго говоря такую функцию в данном виде написать нельзя, потому как при вызове функции переменная уже должна быть "разименована", а значит мы так или иначе будем работать с ее значением или с какими-то косвенными сайд эффектами вызванными обращением к ней.
Сначала рассмотрим несколько решений с изменением сигнатуры: они попроще и в каком-то смысле более логичны.
есть как минимум 2 категории решений с изменением сигнатуры:
- функция будет принимать строку
- функция будет принимать функцию
При этом проблема того, что все падает до попадания в функцию, пропадает сама собой.
вот несколько решений:
1) класс решений построенный на анализе кода в рантайме:
решение заключается в том, что вызываемая функция анализирует свой код (а также код функции которая вызвала ее и т.д.)
https://stackoverflow.com/questions/2051678/gettin... вот тут есть частчный разбор такого решения.
Приводить его в виде кода я не буду :)
Плюсы:
- вроде как решает задачу
- сигнатура изменилась не сильно
Минусы:
- крайне жирное решение с точки зрения производительности
- сигнатура не та, что просили
- нельзя использовать 'use strict', так как решения возможно будут завязаны на arguments.callee.caller
Уровень безумия: достаточно безумное
2) решение построенное на том, что приниматься будет функция:
isExists(() => someVar); // такая будет сигнатура
Это решение хоть и меняет сигнатуру, но ее возможно сохранить максимально подобной желаемой
Одна из возможных реализаций:
function isExists(someVarFn){
try {
someVarFn();
} catch(e) {
if (e instanceof ReferenceError) {
return false;
}
throw e;
}
return true;
}
Примеры использования:
function f1() {
let x;
console.log(isExists(() => x)); // => true
console.log(isExists(() => y)); // => false
}
function f2() {
let x;
console.log(isExists(() => x)); // => true
console.log(isExists(() => y)); // => false
}
f1();
f2();
Плюсы:
- плюс-минус адекватная производительность
- довольно компактно
Минусы:
- несильно меняется сигнатура функции
- api получается несколько не очевидным
Уровень безумия: почти адекватное
3) решения с использованием eval:
Суть похожа на решение #2, но с другим api:
function isExists(varName, fn){
return fn(`(()=>{
try {
const nop = ${varName};
} catch(e) {
if (e instanceof ReferenceError) {
return false;
}
throw e;
}
return true;
})()`);
}
function f1() {
let x;
console.log( isExists('x', c => eval(c)) ); // => true
console.log( isExists('y', c => eval(c)) ); // => false
}
function f2() {
let y;
console.log( isExists('x', c => eval(c)) ); // => false
console.log( isExists('y', c => eval(c)) ); // => true
}
f1();
f2();
Плюсы:
- сложнее чем в №2 передать какую-то фигню
Минусы:
- сигнатура отлиается сильнее и уродливей
- сложнее / жирнее / больше чем №2
Уровень безумия: безумненько
Решения с сохранением сигнатуры:
4) магия es6 Proxies + with
класс решений строится на стратегии: слушать доступ к глобальному объекту и если он происходит и в нем нет переменной с таким именем - такой переменной нет:
const pseudoUndefined = {};
const scopeTrap = new Proxy(window, {
has() {return true},
get(target, prop) {
if (prop in window) {
return window[prop];
} else {
return pseudoUndefined;
}
}
})
function isExists(someVar) {
return someVar !== pseudoUndefined;
}
// весь код использующий функцию должен быть обернут в такой with :(
with (scopeTrap) {
let y;
console.log( isExists(x) ); // => false
console.log( isExists(y) ); // => true
}
Плюсы:
- мы сохранили желаемую сигнатуру! Ура!
Минусы:
- теперь вместо исключения будет получаться что-то совсем левое. Хорошего дебагга! :D
- накладные расходы производительности
- все нужно обернуть в with
- необходима поддержка es6+
Уровень безумия: с точки зрения реализации - задача подъемная. использовать в бою - а вы знаете толк!
Решения с предобработкой:
Первые две категории решений исходят из того, код (включая код вызовов функции) будет исполняться без изменений напряму интерпретатором. Если развязать себе руки, то открывается целый спектр новых решений:
5) первое, что приходит в голову с приобретением такой свободы - это, очевидно... написание своего языка программирования компилируемого в js / своего интерпретатора js и т.д.
В рамках этого решения особого труда (относительно всего прочего) не составит реализовать такую функцию нативно.
Плюсы: ...
Минусы: ...
(Без комментариев)
Уровень безумия: я думаю все понятно
...
есть и более сдержанные решения, например, используя compile-time можно оптимизировать предыдущие решения:
6) можно используя парсер составить дерево областей видимости (хэш-таблица [функция <-> список переменных]), привязать каждую область к функции, через позицию в исходном коде (возможно придется сохранять дополнительную информацию). А дальше все это интегрировать с решением №1.
В итоге:
Плюсы относительно (#1):
- относительно хорошая производительность
Минусы:
- сложно
- дорого
- требует compile-time
Уровень безумия: стало еще безумнее, чем было :)
7) написать (или найти) плагин для babel'я (или чего-то другого), который будет трансформировать доступ к переменным в особую функцию
Плюсы относительно (#1):
- достигнут результат
Минусы:
- требует поддержки
- требует compile-time
- упадет производительность (вероятнее всего существенно)
Уровень безумия: буднечно безумное