Konspekt
Advanced JavaScript
Заметки
You don’t know JS
Область видимости и замыкания
1. Что такое область видимости?
Область видимости — набор правил поиска движком переменных по их идентификатору
Этапы «упрощенной» компиляции:
- Разбиение на лексемы (Tokenizing/Lexing)
- Парсинг — превращение лексем в AST (abstract syntax tree)
- Генерация кода — превращение AST в исполняемый код
Сущности, участвующие в обработке программы:
- Движок — отвечает за компиляцию и выполнение программы
- Компилятор — парсит и генерирует код
- Область видимости — собирает и обсулживает список поиска всех объявленных идентификаторов
var a = 2
Компилятор:
- Разбивает на лексемы
- Парсит в дерево
- Обращается к области видимости с проверкой на существование а в коллекции
- Генерирует код для движка
Движок:
- Спрашивает насчет a у области видимости, если есть — присваивает значение 2, если нет — идет в область выше до глобальной, если нет в глобальной — исключение
Типы поиска:
- LHS — когда переменная появляется с левой стороны операции присваивания (цель присваивания): a = 2
- RHS — с правой стороны (источник присваивания): console.log(a)
Если RHS-поиск не найдет переменную в любой из вложенных областей видимости — возврат RefrenceError
LHS в случае отсутствия переменной создаст ее в глобальной области видимости (если в strict mode — RefrenceError)
TypeError — попытка выполнения нелегального действия с переменной
2. Лексическая область видимости
Определяется во время разбора на лексемы. Основана на том, где написаны блоки области видимости.
Динамическая ОВ — определяется во время выполнения. Основана на том, откуда вызываются функции. В JavaScript отсутствует, механизм this немного похож на ДОВ.
Обман ЛОВ — with , eval(..)
Лишают движок возможности оптимизации во время компиляции
3. Область видимости: функции против блоков
- Область видимости из функции
- IIFE создает область видимости
- Блоки как области видимости (try/catch (в catch), let/const)
4. Всплытие переменных (Hoisting)
Объявления переменных и функций переезжают в начало соответствующих областей видимости, присваивания остаются.
Остерегаться дублей объявлений.
5. Замыкание области видимости
Замыкание — когда функция имеет доступ к ЛОВ, даже если она выполняется вне своей ЛОВ. Конкретно, замыкание — ссылка на эту ЛОВ.
Простой пример:
function foo() {
var a = 2;
return function() { console.log(a); }
}
var bar = foo();
bar(); // 2
Циклы + замыкания: IIFE с передачей итератора, let (определяется для каждой итерации)
Паттерн проектирования «Модуль»:
function Module() {
var a = 2;
function doSomething() { console.log(a) }
function doAnother() { ... }
return {
doSomething: doSomething,
doAnother: doAnother
};
}
foo = Module(); foo.doSomething() // 2
Требования:
- Должна быть вызвана внешняя окружающая функция
- Окружающая функция должна возвращать хотя бы одну внутреннюю функцию, у которой есть замыкание на приватную область видимости
ES6-модули, определяются в отдельных файлах. Импорт:
- функции — import hello from “module”
- модуля целиком — module foo from “module”
This и прототипы объектов
1. Что такое this?
this — привязка, которая создается во время вызова функции, и на что она ссылается определяется тем, где и при каких условиях функция была вызвана.
2. Определение this по месту и способа вызова
- Если функция вызвана с new — используется только что созданный объект
- Вызвана с помощью call / apply / bind — используется указанный объект
- Вызвана с объектом контекста, владеющего вызовом функции obj.func() — используется этот объект контекста this == obj
- По умолчанию: undefined в strict mode , в остальных global
Безопасное игнорирование привязки — передача в call / apply / bind ø = Object.create(null)
Лексический this: стрелочные функции ES6 используют ЛОВ для привязки this , являются заменой self = this в до-ES6 коде
Просто функции обратного вызова теряют свою привязку, так как передается ссылка на функцию обратного вызова без контекста ее исполнения.
Вызов функции с new:
- Создается новый объект
- Сконструированный объект связывается с прототипом
- Сконструированный объект устанавливается как привязка this для этого вызова функции
- Возвращает сконструированный объект (исключение, если возвращается свой собственный альтернативный объект)
3. Объекты
Декларативное (литеральное) создание — {} , создание конструктором — new Object()
Основные типы: string, number, boolean, null, undefined, object
Подтипы объектов, встроенные объекты: String, Number, Boolean, Object, Function, Array, Date, RegExp, Error
Автоматическое преобразование литерала в объект-обертку при обращении к методам: 32.359.toFixed(2)
Вычисляемые имена свойств ES6:
var prefix = "foo";
var myObject = { [prefix + "bar"]: "hello" }; // myObject["foobar"] == hello
При работе со свойствами объекта движок вызывает стандартные операции [[Get]] и [[Put]]
У свойств есть характеристика, оправляемые через дескриптор свойств:
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true
});
Object.getOwnPropertyDescriptor( myObject, "a" );
Управление изменияемостью объекта и свойств:
- writable: false и configurable: false — неизменяемое свойство
- Object.preventExtensions(..) — запрет на добавление новых свойств
- Object.seal(..) — запечатывание: 2) с configurable: false, writable: true для всех свойств
- Object.freeze(..) — заморозка: 3) с writable: false для всех свойств
Геттеры, сеттеры:
var myObject = {
get a() { return this._a_; },
set a(val) { this._a_ = val * 2; }
};
myObject.a = 2;
myObject.a; // 4
Собственный итератор для перебора объекта:
var myObject = { a: 2, b: 3 };
Object.defineProperty(myObject, Symbol.iterator, {
enumerable: false,
writable: false,
configurable: true,
value: function() {
var o = this;
var idx = 0;
var ks = Object.keys( o );
return {
next: function() {
return {
value: o[ks[idx++]],
done: (idx > ks.length)
};
}
};
}
});
// перебираем `myObject` вручную
var it = myObject[Symbol.iterator]();
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { value:undefined, done:true }
// перебираем `myObject` с помощью `for..of`
for (let v of myObject) {
console.log(v);
}
// 2
// 3
Таким образом, можно использовать for..of для объекта: ищет @@iterator, содержащий метод next()
4. В JS нет классов
Класс — шаблон проектирования.
- Наследование
- Полиморфизм — переопределение/расширение родительских свойств/методов
- Инкапсуляция
В традиционных объектно-ориентированных языках ребенок получает копию содержимого родителя при наследовании. В JS ребенок не получает копию автоматически. Придется писать костыли (mixin), которые принесут дополнительные сложности.
5. Прототипы
Внутрення ссылка [[Prototype]] некоторого объекта задает направление поиска для операции [[Get]]. Каскад ссылок от объекта к объекту образует «цепочку прототипов».
У обычных объектов есть встроенный объект Object.prorotype
Установка и затенение свойств:
obj.foo = "bar";
- Если у obj есть обычное свойство foo, то изменяется значение существующего свойства
- Если у obj и цепочки прототипов нет свойства, то свойство со значением добавляется в obj
- Если свойство находится выше по цепочке и не отмечено как {writable: false} , то новое свойство добавляется в obj (затенение)
- Если свойство находится выше по цепочке и отмечено как {writable: false} , то ничего не произойдет, в strict ошибка
- Если свойство находится выше по цепочке и является сеттером, то всегда будет вызываться сеттер
В 4 и 5 случаях для затенения вместо присваивания использовать Object.defineProperty(..)
Неявное затенение:
var obj1 = {a: 2};
var obj2 = Object.create(obj1);
obj2.a++; //obj1.a == 2; obj2.a == 3 (obj2.a = obj2.a + 1)
Способы связать два объекта:
- function Foo() { .. }
- var a = new Foo();
- Object.getProrotypeOf(a) === Foo.prototype; // true
- Вызов new Foo() создает новые объект, связанный внутренней ссылкой [[Prototype]] с объектом Foo.prototype.
- Object.create(somObj) — создает пустой объект с ссылкой [[Prototype]] на someObj.
.constructor — обратная ссылка на функцию, с которой связан объект. Не безопасен! Может принадлежать другому в цепочке объекту. Ручное восстановление свойства.
Делегирование («прототипом наследование»):
- Bar.prototype = Object.create(Foo.prototype)
- Object.setPrototypeOf(Bar.prototype, Foo.prototype); // ES6+
Неудачные
- Bar.prototype = Foo.prototype; // Замена ссылки на объект
- Bar.prototype = new Foo(); // Работает хорошо, но с побочными эффектами, если прописаны
Интроспекция
- instanceof (учитывать особенности при использовании с bind)
- Foo.prototype.isPrototypeOf(a);
- Object.getPrototypeOf(a);
- a.__proto__ === Foo.prototype // true
Частичный полифил Object.create(..):
if (!Object.create) {
Object.create = function(o) {
function F(){}
F.prototype = o;
return new F();
};
}
Совет использовать шаблон делегирования:
function Foo() { this.doCool = function() { this.cool() }; }
Foo.prototype.cool = function() { console.log("cool") };
var a = new Foo();
a.doCool(); // "cool"
или
var anotherObject = {
cool: function() { console.log("cool"); }
};
var myObject = Object.create(anotherObject);
myObject.doCool = function() { this.cool(); };
myObject.doCool(); // "cool"
6. Делегирование поведения
Особенности OLOO стиля программирования:
- Только объекты, связанные с другими объектами
- Свойства хранятся в делегирующих объектах, а не делегатах
- Избегать одинаковых имен свойств на разных уровнях цепочки. При затенении код становится хрупким.
- Доступны методы общего назначения из делегатов
Классический «прототипный» OO стиль, пытаемся в class-way:
function Foo(who) { this.me = who; }
Foo.prototype.identify = function() { return "I am " + this.me; };
function Bar(who) { Foo.call( this, who ); }
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() { alert( "Hello, " + this.identify() + "." ); };
var b1 = new Bar( "b1" ); var b2 = new Bar( "b2" );
b1.speak(); b2.speak();
OLOO стиль:
var Foo = {
init: function(who) { this.me = who; },
identify: function() { return "I am " + this.me; }
};
var Bar = Object.create( Foo );
Bar.speak = function() { alert( "Hello, " + this.identify() + "." ); };
var b1 = Object.create( Bar ); b1.init( "b1" );
var b2 = Object.create( Bar ); b2.init( "b2" );
b1.speak(); b2.speak();
Реальные примеры кода и до конца главы
7. class в ES6
class Widget {
constructor() { … }
render() { … }
}
class Button extends Widget {
constructor() { super(); … }
render() { super.render(); … }
onClick(e) { … }
}
Какие проблемы решает:
- Больше нет отсылок к .prototype
- Унаследование (extends) вместо Object.create() или установки .__proto__
- super(..) дает относительный полиморфизм
- Не позволяет указать свойства, только методы. Защищает от ошибок
- extends позволяет расширить даже встроенные подтипы
Глюки:
- Это всего лишь сахар поверх существующего механизма делегирования, классы ненастоящие
- Не предоставляет способа объявить свойства экземпляра класса, приходится делать через .prototype
- super в некоторых случаях неочевидно привязывается, может понадобиться привязать вручную через toMethod (код ниже)
- Не является статическим!
class P { foo() { console.log( "P.foo" ); } }
class C extends P { foo() { super(); } }
var c1 = new C();
c1.foo(); // "P.foo"
var D = { foo: function() { console.log( "D.foo" ); } };
var E = { foo: C.prototype.foo };
// Ссылка от E к D для делегирования
Object.setPrototypeOf( E, D );
E.foo(); // "P.foo"
Исправляем привязку:
var D = { foo: function() { console.log( "D.foo" ); } };
var E = Object.create( D );
// вручную связать `[[HomeObject]]` из `foo` в виде
// `E`, а `E.[[Prototype]]` — это `D`, так что
// `super()` — это `D.foo()`
E.foo = C.prototype.foo.toMethod( E, "foo" );
E.foo(); // "D.foo"
Типы и синтаксис
1. Типы
Встроенные типы: null, undefined, boolean, string, number, object, symbol (ES6)
var a;
a; // undefined
b; // ReferenceError: b is not defined
if (DEBUG) console.log( "Debugging is starting" ); // ошибка
if (typeof DEBUG !== "undefined") console.log( "Debugging is starting" ); // безопасно
if (window.DEBUG) // безопасно, т.к. обращение к свойству объекта, вернет undefined, если нету
2. Некоторые встроенные типы
Обработка массивоподобных объектов
var arr = Array.prototype.slice.call(arguments);
var arr = Array.from(arguments); // ES6
Правильный способ обращения к символу строки str.charAt(1)
join и map можно одолжить у массива для строки
var s = Array.prototype.join.call( str, "-" );
0.1 + 0.2 === 0.3 // false
if (!Number.EPSILON) Number.EPSILON = Math.pow(2,-52);
function numbersCloseEnoughToEqual(n1,n2) {
return Math.abs( n1 - n2 ) < Number.EPSILON;
}
var a = 0.1 + 0.2; var b = 0.3;
numbersCloseEnoughToEqual( a, b );
Безопасной макисмальное целое число Number.MAX_SAFE_INTEGER: 2^53 - 1
Оператор void сознает значение undefined из любого значения
NaN — спец. числовое значение, не равняется самому себе
ES6 Абсолютное равенство Object.is(..)
var a = 2 / "foo"; var b = -3 * 0;
Object.is( a, NaN ); // true
Object.is( b, -0 ); // true
Object.is( b, 0 ); // false
Простые значения копируются, сложные (массивы, объекты, функции) передаются по ссылке
3. Обертки (Natives)
Смысла использовать обертки просто так нет. В JS все оптимизировано, круто работает и без моих кривых рук.
Unboxing: явно var a = new String('abc'); a.valueOf() //'abc', неявно, когда оператор требует примитив
var a = new Array(3); с одним аргументом не использовать
Использовать литеральную нотацию, даже для регулярок (Можно new RegExp, когда шаблон опр. динамически)
Date() и Error() наиболее полезные конструкторы
Date() без new возвращает строку с текущим временем и датой
By documentation convention, String.prototype.XYZ is shortened to String#XYZ
4. Приведение (Coercion)
Приведение только к (скалярным) примитивам string, number, boolean
- Неявное приведение (не стоит изучать весь этот бред, просто запомни безопасные и легко читаемые способы)
- Явное приведение
a || b; // roughly equivalent to: a ? a : b;
a && b; // roughly equivalent to: a ? b : a;
Таблица эквивалентности:
5. Грамматика
Текст