портал
Эта серия статей подходит для фронтенд-разработчиков, которые хотят систематически учиться, продвигать JavaScript или грамотность.
Переменные, область видимости и память
- Использование примитивных и эталонных значений с переменными
- понять контекст выполнения
- Понимание сборки мусора
По сравнению с другими языками, переменные JavaScript имеют свободный тип, а значение и тип данных переменных могут быть произвольно изменены в течение жизненного цикла скрипта. Это мощно, но также создает много проблем.
Примитивное значение против эталонного значения
ECMAScript содержит два типа данных: примитивные значения и эталонные значения. Примитивные значения — это простейшие данные, а ссылочные значения — это объекты, состоящие из нескольких значений.
Примитивные значения включают Undefined, Null, Boolean, Number, String и Synbol. Доступ к примитивным значениям осуществляется по значению, манипулируя фактическим значением, хранящимся в переменной.
Ссылочное значение — это объект, хранящийся в памяти. JavaScript не допускает прямого доступа к памяти, поэтому, когда вы манипулируете объектом, вы манипулируете ссылкой на этот объект, а не на фактический объект. Следовательно, доступ к переменным, которые ссылаются на значения, осуществляется по ссылке.
динамические свойства
Динамические свойства — это свойства, которые можно динамически добавлять к переменной ссылочного типа после ее создания. Примитивные значения не могут иметь атрибутов, хотя добавление атрибутов к примитивным значениям не вызовет ошибки.
// 对象
const obj = new Object();
// 动态添加属性
obj.name = 'lyn'
console.log(obj.name) // lyn
// 原始值
const vari = 'test'
vari.name = 'lyn'
console.log(vari.name) // undefined
копировать значение
Помимо того, что они хранятся по-разному, примитивные и ссылочные значения различаются при копировании переменных. Поскольку при копировании копируется сама переменная, исходное значение будет скопировано полностью, и две скопированные переменные не будут мешать друг другу; но переменная ссылочного значения хранит адрес памяти, поэтому фактически копируется указатель, после копирования старое и новые переменные будут указывать тот же объект, хранящийся в куче памяти.
Исходное значение
// 复制后的两个变量可独立使用,互不干扰
let num1 = 5;
let num2 = num1;
console.log(num1, num2) // 5 5
num1 = 6;
num2 = 8
console.log(num1, num2) // 6 8
исходная величина
// 复制后两个变量操作的是同一个对象
const obj1 = { t: 't' }
const obj2 = obj1
console.log(obj1, obj2) // {t: "t"} {t: "t"}
obj1.t = 'tt'
console.log(obj1, obj2) // {t: "tt"} {t: "tt"}
передать параметры
Точки доступа к переменным ECMAScript: по ссылке и доступ по значению доступа, но параметры передаточной функции передаются по значению, включая значение ссылочного типа.
При передаче аргумента по значению значение копируется в локальную переменную функции (именованный аргумент или, на языке ECMAScript, слот в объекте arguments). Чтобы объяснить на примере:
const gNum = 10
function fn(num) {
num += 10;
console.log(num) // 20,局部变量 num 的值改
console.log(gNum) // 10,外层的 num 没有变
}
fn(num)
В этом примере показано, что примитивные значения передаются по значению
const gObj = { t: 't' }
function fn(obj) {
console.log(obj) // { t: 't' }
obj.t = 'tt'
// 这里访问外层的 gObj 的值变了只能说明 obj 和 gObj 的值保存的是同一个指针
console.log(gObj) // { t: 'tt' }
// 覆写 obj 变量的值
obj = { c: 'c' }
// 这里两个不一样,只能说明 obj 存储的值(指针)变量
console.log(obj) // { c: 'c' }
console.log(gObj) // { t: 'tt' }
}
fn(obj)
Этого примера достаточно, чтобы показать, что переменные ссылочного типа также передаются по значению при передаче функции, потому что если она передается по ссылке, то при перезаписывании obj должен перезаписываться и gObj.
На самом деле ничего из вышеперечисленного не важно.Важно понимать, что хранится в исходной переменной-значении и переменной ссылочного типа.Если вы это поймете, то поймете все.
определить тип
Существует 5 способов определить тип переменной в ECMAScript: оператор typeof, оператор instanceof, метод Array.isArray(), свойство конструктора, метод Object.prototype.toString().
typeof очень полезен для оценки типов строк, чисел, логических значений, undefined и функций, но не для объектов, нулей, массивов и т. д.
instanceof и конструктор обычно используются для определения того, является ли указанная переменная экземпляром объекта.
Метод Array.isArray(variable) используется для определения того, является ли указанная переменная массивом, что может решить проблему преувеличения кадра.
Метод String.prototype.toString.call() — идеальный способ определить тип переменной.
// typeof
console.log(typeof 'test') // string
console.log(typeof 2) // number
console.log(typeof true) // boolean
console.log(typeof undefined) // undefined
// 以下类型用 typeof 不好使
console.log(typeof null) // object
console.log(typeof {}) // object
console.log(typeof []) // object
console.log(typeof new Date()) // object
console.log(typeof function () {}) // function
console.log(typeof Array) // function
// instanceof 和 constructor
const d = new Date()
console.log(d instanceof Date) // true
console.log(d instanceof Array) // false
console.log(d.constructor === Date) // true
console.log(d.constructor === Array) // false
// Object.prototype.toString.apply(variable),返回 [object Xxx]
console.log(Object.prototype.toString.apply(2)) // [object Number]
console.log(Object.prototype.toString.apply(null)) // [object Null]
console.log(Object.prototype.toString.apply(Array)) // [object Function]
console.log(Object.prototype.toString.apply({})) // [object Object]
console.log(Object.prototype.toString.apply(d)) // [object Date]
ECMA-262 утверждает, что любой объект, реализующий внутренний метод [[Call]], должен возвращать функцию при обнаружении typeof.
контекст и область выполнения
Контекст выполнения (далее именуемый «контекст») определяет, к каким переменным данных и функциям можно получить доступ, и как они себя ведут. Каждый контекст имеет связанныйпеременный объект, все переменные и функции, определенные в контексте, существуют в этом объекте. Хотя объект переменной недоступен из кода, он используется при обработке данных в фоновом режиме. Контекст делится на три типа: глобальный контекст, контекст функции и контекст внутри вызова eval.
В зависимости от хост-окружения, реализующего ECMAScript, объекты глобального контекста могут быть разными, например, в браузере глобальным контекстом является объект окна, а все глобальные переменные и функции, объявленные через var, станут свойствами и методами объект окна. Объявления верхнего уровня с использованием let и const не определены в глобальном контексте, но имеют такое же влияние на разрешение цепочки областей видимости. Контекст будет уничтожен после завершения выполнения его левого и правого кода, а глобальный контекст будет уничтожен перед выходом из приложения, например, закрытием веб-страницы и браузера.
Каждая функция имеет свой контекст. Когда выполнение кода входит в функцию, контекст функции помещается в стек контекста. После того, как функция будет выполнена, стек контекста выведет контекст функции и вернет управление предыдущему контексту выполнения. Поток выполнения программы ECMAScript управляется через этот стек контекста.
Функция eval может устанавливать глобальный контекст при вызове, формируя таким образом отдельный контекст. Например, песочница JS фреймворка qiankun использует функцию eval.
Когда код контекста будет выполнен, он создаст переменный объектцепочка прицелов. Переменный объект глобального контекста всегда находится в конце цепочки областей видимости, а переменный объект текущего исполняемого контекста находится в начале цепочки областей видимости. Разрешение идентификатора во время выполнения кода выполняется путем поиска в обратном направлении, начиная с переднего конца цепочки областей видимости (объекты в цепочке областей действия также имеют объекты-прототипы, поэтому поиск может включать цепочку прототипов каждого объекта), если поиск К переменному объекту глобального контекста идентификатор не найден, что указывает на то, что он не объявлен.
Поиск идентификаторов в цепочке областей действия имеет определенные накладные расходы на производительность. Доступ к локальным переменным выполняется быстрее, чем доступ к глобальным переменным, поскольку нет необходимости переключать области. Однако движок JavaScript выполняет большую работу по оптимизации поиска идентификаторов, и в будущем эта разница может стать незначительной.
var color = "blue";
function changeColor() {
let anotherColor = "red";
function swapColors() {
let tempColor = anotherColor;
anotherColor = color;
color = tempColor;
// 这里可以访问color、anotherColor和tempColor
}
// 这里可以访问color和anotherColor,但访问不到tempColor
swapColors();
}
// 这里只能访问color
changeColor();
Каждый прямоугольник представляет контекст, и внутренний контекст может получить доступ ко всему из внешнего контекста через цепочку областей видимости, но внешний контекст не может получить доступ ни к чему из внутреннего контекста.
Расширение цепочки областей действия (расширение)
Следующие два оператора добавляют временный контекст в начало цепочки областей (расширяют цепочку областей) при выполнении, и этот контекст будет удален после завершения выполнения кода.
-
блок catch для оператора try/catch
Оператор catch создает новый объект переменной (ошибка), содержащий объявление вызываемого объекта ошибки.
-
с заявлением
Оператор with добавляет указанный объект в начало цепочки областей видимости.
function fn() {
const qs = '?dev=true'
with(location) {
// var 无法声明块级作用域变量,所以 url 会成为 fn 函数上下文的一部分
var fnUrl = href + qs + '&vartest=1'
// const 不存在 var 的问题,因为 const 和 let 可以声明块级作用域变量
const urlTest = href + qs
console.log(urlTest)
}
// 可以正常访问
console.log(fnUrl)
// ReferenceError: urlTest is not defined
// console.log(urlTest)
}
fn()
вывоз мусора
В таких языках, как C и C++, управление памятью должен выполнять сам разработчик. JavaScript — это язык со сборкой мусора, который выделяет память и восстанавливает простаивающие ресурсы посредством автоматического управления памятью. Идея проста: определить, какая переменная больше не будет использоваться, и освободить занимаемую ею память. Это периодический процесс, и сборщик мусора запускается автоматически через регулярные промежутки времени (или некоторое запланированное время сбора во время выполнения кода).
Локальные переменные в функции существуют на время выполнения функции. В этот момент память стека (кучи) выделит место для хранения соответствующих значений переменных. Функция использует переменную внутри, а затем завершает работу. В этот момент локальная переменная больше не нужна, и память, которую она занимает, может быть освобождена. Тогда не всегда все так очевидно. Сборщик мусора должен отслеживать, какие переменные все еще используются, а какие нет, чтобы освободить память. Существуют разные реализации того, как помечать неиспользуемые переменные. Однако в истории развития браузеров использовались две основные стратегии разметки: очистка разметки и подсчет ссылок.
очистка разметки
Когда сборщик мусора запускается, он помечает все переменные в памяти (существует множество способов пометки, например, сохранение двух списков переменных в контексте и вне контекста, инвертирование бита при попадании переменной в контекст и т. д.). Затем он снимает пометки со всех переменных в контексте и переменных, на которые ссылаются переменные в контексте. Переменные, помеченные после этого, можно удалить, потому что ни одна из переменных в контексте не может получить к ним доступ. Затем сборщик мусора выполняет очистку памяти, уничтожая все отмеченные переменные и освобождая их память.
В настоящее время практически все браузеры используют очистку разметки (или ее вариант) в своей реализации JavaScript, за исключением того, как часто запускается сборщик мусора.
подсчет ссылок
Никакие браузеры больше не используют эту стратегию разметки. Идея состоит в том, чтобы отслеживать, сколько раз на каждое значение ссылаются (объявите переменную и присвойте ей значение, и это значение будет иметь счетчик ссылок, равный 1). Когда счетчик ссылок значения равен 0, это означает, что значение бесполезно и его память может быть восстановлена. При следующем запуске сборщика мусора память для указанного значения 0 будет освобождена.
У подсчета ссылок есть серьезная проблема: циклические ссылки. Так называемая циклическая ссылка означает, что объект A имеет указатель на объект B, а объект B также ссылается на объект A. Например:
function probleFn() {
const objA = new Object();
const objB = {}
objA.key = objB
objB.key = objA
}
В примере objA и objB ссылаются друг на друга через свои соответствующие свойства, что означает, что счетчик ссылок всегда будет равен 2. При стратегии пометки-очистки проблем нет, потому что после запуска функции два объекта больше не находятся в области видимости и могут быть собраны сборщиком мусора. В соответствии со стратегией подсчета ссылок objA и objB все еще будут существовать после завершения функции, потому что счетчик ссылок никогда не будет равен 0. Если функция вызывается много раз, это приведет к тому, что много памяти никогда не будет освобождено. вызвать утечку памяти
представление
Сборщик мусора будет выполняться периодически, и его стратегия планирования времени очень важна.Неправильная стратегия планирования не только не улучшит производительность, но значительно замедлит скорость рендеринга и частоту кадров. Разработчик не знает, когда будет запущен сборщик мусора, поэтому лучше всего сделать это при написании кода: всякий раз, когда сборщик мусора запускается, он может закончить свою работу как можно быстрее, то есть вручную освободить переменную после него израсходовано (установить его). равно нулю).
Современные сборщики мусора решают, когда выполняться, основываясь на проверке среды выполнения JavaScript. Механизм обнаружения зависит от движка, но в основном зависит от размера и количества выделенных объектов. Например, в сообщении в блоге 2016 года команда V8 заявила, что после полной сборки мусора стратегия роста кучи V8 определяет, когда снова собирать мусор, на основе количества активных объектов плюс некоторый резерв.
управление памятью
JavaScript работает в среде, в которой особое внимание уделяется управлению памятью и сборке мусора. Память, выделяемая для браузеров, как правило, намного меньше, чем для программного обеспечения для настольных компьютеров, и даже меньше для мобильных браузеров. Это делается из соображений безопасности, чтобы веб-страница с большим количеством JavaScript не исчерпала системную память и не привела к сбою операционной системы.
В случае ограниченного объема памяти сохранение только необходимых данных в выполняющемся поколении может повысить производительность страницы. Если данные больше не используются, они разыменовываются, вручную присваивая им значение null. Эта операция хорошо работает для глобальных переменных и глобального объекта и его свойств. Локальные переменные автоматически разыменовываются, когда они выходят за пределы области видимости.
function fn() {
// 函数执行结束,num 自动解除引用
const num = 3
return num
}
const fnReturn = fn()
console.log(fnReturn)
// 全局变量使用完以后手动解除引用
fnReturn = null
Отмена применения значения не приводит к автоматическому освобождению связанной памяти. Ключом к разыменованию является гарантия того, что значение больше не находится в контексте, поэтому оно будет собрано при следующей сборке мусора.
Улучшите производительность с помощью объявлений const и let
Переменные, объявленные с помощью const и let, имеют блочную область действия. По сравнению с var, в случае, когда область действия на уровне блока заканчивается раньше, чем область действия функции, переменные, объявленные с помощью этих двух ключевых слов, могут использоваться сборщиком мусора раньше. соответствующую память как можно скорее.
Скрыть классы и удалить операции
Динамическая природа JavaScript, как правило, подрывает стратегии оптимизации «скрытого класса» браузера, такие как динамические свойства для добавления и удаления атрибутов.
V8 использует «скрытые классы» при компиляции интерпретируемого кода JavaScript в машинный код. Во время выполнения V8 связывает созданные объекты со скрытыми классами, чтобы отслеживать характеристики их свойств. Объекты, которые могут использовать один и тот же скрытый класс, работают лучше.
function Article() {
this.title = 'test'
}
const obj1 = new Article()
const obj2 = new Article()
V8 настраивается за кулисами, так что два экземпляра, obj1 и obj2, используют один и тот же скрытый класс, поскольку оба экземпляра используют один и тот же конструктор и прототип.
Скрытый класс — это анонимный класс, объявленный самим V8 в фоновом режиме, с режимом быстрого доступа для повышения производительности. ПодробностиСсылаться на. В настоящее время его можно просто понимать как класс
Если вы добавите следующую строку кода в это время:
obj2.author = 'Jack'
На этом этапе два экземпляра статьи будут соответствовать двум разным скрытым классам (еще одному классу). Если частота таких операций высока, а сравнение скрытых классов велико, это может оказать существенное влияние на производительность. И это разрушит оптимизацию, вызванную режимом быстрого доступа (вырождение от статического типа к динамическому типу). Динамическое удаление свойства с помощью удаления приводит к той же проблеме.
такЛучшие практикиДа: объявите все свойства одновременно в конструкторе, чтобы не создавать, а затем добавлять свойства; там, где необходимо использовать delete.key, вручную установите для него значение null, например:obj.title = null
.
утечка памяти
Большинство утечек памяти в JavaScript вызваны необоснованными ссылками, такими как:
-
Случайно объявленная глобальная переменная
function fn() { t = 'test' }
Поскольку ключевые слова const, let и var не используются при объявлении переменной t, t будет существовать как атрибут объекта окна.Первоначальное намерение состояло в том, чтобы объявить локальную переменную, и она будет уничтожена после запуска функции. глобальная переменная.
-
Таймеры также могут вызывать утечку памяти, например, функция обратного вызова таймера ссылается на внешние переменные через безопасность.
function fn() { const t = 'test' setInterval(() => { console.log(t) }, 1000) } fn()
Этот 1-секундный таймер приведет к тому, что локальная переменная t функции не будет освобождена после завершения работы функции.
-
Замыкания могут по незнанию вызвать утечку памяти
let outer = function(){ const t = 'test' return function() { console.log(t) } }() // 这行没有执行的话会导致变量 t 的内存始终无法被回收 // outer = null
Существование внешнего приводит к тому, что пространство памяти локальной переменной функции t никогда не освобождается, потому что замыкание всегда ссылалось на него.
Хотя вышеупомянутые пункты могут показаться незначительными, когда вы бесконечно увеличиваете количество задействованных данных, вы обнаружите, что серьезность проблемы намного серьезнее, чем предполагалось.
Статическое размещение и объединение объектов
Благодаря рациональному использованию выделенной памяти удается избежать ненужной сборки мусора, тем самым сохраняя производительность, потерянную при освобождении памяти.
Одним из критериев, по которым браузер решает, когда запускать сборщик мусора, является скорость обновления объектов. Если естьмногоЕсли объект инициализируется, а затем снова выходит из области видимости, браузер запланирует запуск сборщика мусора более агрессивным образом, что приведет к частичной потере производительности из-за большого количества запущенных сборщиков мусора.
В настоящее время стратегия состоит в том, чтобы управлять набором повторно используемых объектов через пул объектов. Приложение может запросить объект из этого пула объектов, установить свойства, использовать его, а затем вернуть его в пул объектов после завершения операции. Поскольку инициализация и уничтожение объекта не происходит в течение всего процесса, сборщик мусора не может обнаружить замену объекта, поэтому программа сбора мусора не будет выполняться часто.
Использование массива для поддержки пула объектов является хорошим выбором, но необходимо соблюдать осторожность, чтобы не вызвать дополнительную сборку мусора, например:
const objArr = new Array(100)
// 假设这时如果已经存储了100个对象
const obj = new Object()
// 这里会带来额外的垃圾回收
objArr.push(obj)
Поскольку размер массивов JavaScript динамически переменная, двигатель удаляет исходный массив размера 100, прежде чем создавать новый массив размером 200. Когда сборщик мусора видит эту операцию удаления, она скоро будет работать, чтобы собрать мусор. Поэтому, чтобы избежать максимально возможной операции динамического распределения, вы можете создать массив достаточного размера во время инициализации, чтобы избежать дополнительной сборки мусора.
Статическое размещение — это экстремальная оптимизация. Если ваше приложение сильно тормозит сборщик мусора, вы можете использовать его для повышения производительности. Но это редкость, и в большинстве случаев это преждевременная оптимизация, так что не стоит ее рассматривать.