Боль нативного JS

JavaScript ECMAScript 6
Боль нативного JS

Чем больше вы страдаете, тем больше вы увидите и услышите. - Гомер

Это старая статья, которую я написал год назад, в этот раз я ее переиздал, потому что оглянулся назад и обнаружил, что в предыдущей статье было много ошибок или непонятных мест. По прошествии года многие вещи, которые я раньше не понимал, вдруг стали ясны, может быть, это рост на практике.

Учитель Сяоган

Основные типы и типы объектов

Типы данных в js делятся на базовые типы и типы объектов (базовые типы также называются примитивными типами, типы значений и типы объектов также называются ссылочными типами).Основные типы следующие:

  • number
  • string
  • boolean
  • null
  • undefined
  • symbol (es6)
  • bigInt (es6+)

Типы объектов включают объектыobject, множествоarray, функцияfunctionЖдать:

  • object
  • function
  • array
  • set,weakSet (es6)
  • map,weakmap(es6)

базовый тип

stringТипы — это строки.В дополнение к одинарным и двойным кавычкам в es6 были введены новые обратные кавычки ` ` для хранения строк. Расширенная функция обратных кавычек доступна с${…}Встраивайте переменные и выражения в строки. Способ применения следующий:

let n = 3
let m = () => 4
let str = `m + n = ${m() + n}` // "m + n = 7"

numberЗначения типа включают целые числа, числа с плавающей запятой,NaN,InfinityЖдать. вNaNТип — единственный тип в js, который не равен самому себе, при возникновении неопределенной математической операции он вернетNaN,Такие как:1+'asdf',Number('asdf').浮点数的运算可能会出现如0.1 + 0.2 !== 0.3Проблема, возникающая из-за точности операций с плавающей запятой, обычно используетсяtoFixed(10)может решить такие проблемы.

boolean,string,number,symbol,bigIntСамо собой разумеется, что в качестве базового типа не должно быть никакой функции для вызова, потому что базовый тип не имеет цепочки прототипов для предоставления методов. Однако эти три типа могут вызыватьtoStringИ другие методы на объекте прототипа:

true.toString() // 'true'
`asdf`.toString() // 'asdf'
NaN.toString() // 'NaN'
Symbol(1).toString() // 'Symbol(1)'
bigInt(1).toString() // '1'

Вы можете сказать, почему тогда числа1нельзя назватьtoStringметод? На самом деле, это не невозможно вызвать:

1 .toString()
1..toString()
(1).toString()

Все три вышеуказанных вызова возможны, первая точка после числа будет интерпретироваться как десятичная точка, а не точечный вызов. Просто такое использование не рекомендуется, да и смысла в этом нет.

Почему примитивные типы могут напрямую вызывать методы объектных типов? На самом деле, когда движок js анализирует приведенный выше оператор, он анализирует эти три основных типа какобъект-оболочка(Это следующееnew String()), а объект-оболочка — это тип объекта, который можно вызватьObject.prototypeспособ выше. Примерный процесс выглядит следующим образом:

'asdf'.toString()  ->  new String('asdf').toString()  -> 'asdf'

nullСпециальное значение, означающее «нет», «нуль» или «значение неизвестно».

undefinedозначает «не назначен». за исключением случаев, когда переменная была объявлена ​​неназначеннойundefined, если свойство объекта не существуетundefined. Так что по возможности следует избегатьvar a = undefined; var o = {b: undefined}Этот способ написания вместо использованияvar a = null; var o = {b: null}, по умолчанию с «не назначен»undefinedслучаи различают.

SymbolЗначение представляет собой уникальный идентификатор. Можно использоватьSymbol()Создание функции:

var a = Symbol('asdf')
var b = Symbol('asdf')
a === b // false

BigIntИспользуется для представления произвольно большого целого числа, записанного путем добавления строчной буквы n после числа. Первоначально наибольшее число, представленное числом в Javascript, равно 2 в 53-й степени, и если это число будет превышено, точность будет потеряна. Из-за потери точности при преобразовании между Number и BigInt рекомендуется использовать тип BigInt только в том случае, если значение может быть больше 253, а не выполнять преобразование между двумя типами.

тип объекта

Объект — это набор свойств, состоящий из пар ключ-значение, где ключ может быть строкой и символом, а значение может быть любого типа.

Массивы и функции — это специальные объекты, которыеlengthсвойства (функции такжеname,prototypeЖдать). Массивы в основном представляют собой различные методы, см.эта статья. Функции — это первоклассные граждане js и включают в себя много контента, вы можете ссылаться на них.эта статья. В этой статье мы не будем вдаваться в подробности.

Разница между примитивными типами и объектными типами

Переменные хранятся в памяти стека, разница в том, что базовый тип хранит значение в стеке, а объектный тип хранит в стеке указатель на реальный адрес памяти объекта. Реальный адрес памяти, на который указывает этот указатель, фактически находится в куче памяти.

Типы объектов работают в двух случаях:

  • Случай 1, ОбъектАтрибутыОперации добавления/изменения/удаления выполняются в памяти кучи и влияют на все объекты, ссылающиеся на адрес памяти кучи.
const obj = {
  a: 'a',
  b: 'b'
}
const newObj = obj

newObj.c = 'cccc'
newObj.a = 'aaaa'
delete newObj.b
// newObj === obj === { a: 'aaaa', c: 'cccc' }
  • В случае 2 объект переназначается напрямую, а указатель, хранящийся в стеке, изменяется, и объект теряет связь с исходным адресом памяти кучи.
const obj = {
  a: 'a',
  b: 'b',
}
const newObj = obj

newObj = {}
// newObj !== obj

Когда параметр функции является типом объекта, переданный параметр фактически является указателем на объект:

function fn (item) {
  item.c = 'c'
}
const obj = {
  a: 'a',
  b: 'b'
}
fn(obj)
// obj.c === 'c'
// 当执行fn(obj)时,相当于在函数内部新声明了一个变量item,并赋值为obj

Типовое суждение

Оценка типа объекта и типа базового типа различна, и оценка базового типа может использоватьtypeof:

typeof 1 // 'number'
typeof 'asdf' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof BigInt(1) // 'bigint'

можно увидеть, кромеnullДругие основные типы суждений нормальны,typeof(null) === 'object'проверенная временем ошибка в самых первых версиях JSnullИнформация о хранении в памяти000начало, и000начинается с будет оцениваться какobjectТипы. Хотя код внутренней оценки типа был изменен, эту ошибку необходимо сохранить в версии, потому что изменение этой ошибки вызовет ошибки на многих веб-сайтах.

typeofдля типов объектов, кроме возвращаемых функцийfunction, все остальное возвращаетсяobject. Но массив в нашей разработке надо вернутьarrayтипа, такtypeofНе очень подходит для типов объектов. Тип объекта оценки обычно используетсяinstanceof:

var obj = {}
var arr = []
var fun = () => {}
typeof obj // 'object'
typeof arr // 'object'
typeof fun // 'function'
obj instanceof Object // true
arr instanceof Array // true
fun instanceof Function // true

можно увидетьinstanceofОператор может правильно определить тип типа объекта.instanceofПо сути, он оценивает конструктор справаprototypeЕсли объект существует в цепочке прототипов слева, возвращает true, если это так. Итак, будь то массивы, объекты или функции,... instanceof Objectоба возвращаютсяtrue.

Наконец, есть всемогущий метод типа суждения:Object.prototype.toString.call(...), можете попробовать сами.

неявное преобразование типов

Сильная и слабая типизация

Языки со строгой типизацией требуют, чтобы все переменные были определены перед использованием, и как только переменной назначен тип данных, она всегда будет иметь этот тип данных, если она не будет принудительно задана. В строго типизированных языках нет неявных преобразований типов. В отличие от языков со слабой типизацией, в объявлениях переменных не нужно указывать типы, а неявное преобразование типов часто происходит по мере необходимости во время выполнения.

статическая и динамическая типизация

Статические языки проверяются на тип во время компиляции, динамические языки проверяются на тип во время выполнения.

Python — динамический язык, строго типизированный язык (типобезопасный язык), JAVA — статический язык, строго типизированный язык (типобезопасный язык);

JS — это динамический язык со слабой типизацией.Неявные преобразования типов будут происходить между разными типами при определенных обстоятельствах, например, при сравнении на равенство.

Неявное преобразование типов при сравнении на равенство

Сравнение на равенство базовых типов — совпадают ли значения, а сравнение на равенство объектов — совпадают ли ссылочные адреса памяти. Вот интересное сравнение:

[] == [] // ?
[] == ![] // ?

для[] {} function (){}Для таких ссылочных типов, которые не присвоены переменным, они допустимы только в текущем операторе и не равны никаким другим объектам. Потому что просто нет возможности найти указатель на их адрес памяти. так[] == []даfalse.

для[] == ![], потому что он включает неявное преобразование типов, поэтому он намного сложнее.

Правила сравнения равенства различных типов операндов следующие:

  • Сначала определите, сравниваете ли вы null и undefined, и если да, то он вернет true. null и undefined не равны никакому другому значению.
null == undefined // true
null == 0 // false
undefined == 0 // false
  • Определите, являются ли два типа строки и номером, и, если это так, преобразуйте строку на номер;
NaN == NaN // false     NaN不等于任何值
  • Судя по тому, является ли единица логическим, если это так, он превратит логическое значение в число, а затем вынесет решение;
  • Определите, является ли один из них объектом, а другой строкой, числом или символом.Если это так, объект будет преобразован в примитивный тип, а затем оценен.

теперь, чтобы раскрыть[] == ![]вернутьtrueПравда поставил:

[] == ![] // true
/*
 * 首先,布尔操作符!优先级更高,所以被转变为:[] == false
 * 其次,操作数存在布尔值false,将布尔值转为数字:[] == 0
 * 再次,操作数[]是对象,转为原始类型(先调用valueOf(),得到的还是[],再调用toString(),得到空字符串''):'' == 0
 * 最后,字符串和数字比较,转为数字:0 == 0
*/

В JS всего три случая преобразования типов:toNumber,toString,toBoolean. В нормальных условиях правила преобразования следующие:

Примитивное значение/тип тип цели: число результат
null number 0
symbol number бросать неправильно
string number '1'=>1 '1a'=>NaN, с нецифрами какNaN
множество number []=>0 ['1']=>1 ['1', '2']=>NaN
object/function/undefined number NaN
Примитивное значение/тип тип цели: строка результат
number string 1=>'1'
array string [1, 2]=>'1,2'
логическое значение/функция/символ string Исходное значение непосредственно заключено в кавычки, например:'true'
object string {}=>'[object Object]'
Примитивное значение/тип Тип цели: логический результат
number boolean Кроме0,NaNдляfalse, остальныеtrue
string boolean за исключением пустой строкиfalse, остальныеtrue
null/undefined boolean false
тип объекта boolean true

Для получения более подробной информации о преобразовании неявного преобразования типов вы можете увидеть меняэта статья.

область действия и контекст выполнения

объем

Область видимости в js — это лексическая область видимости, которая определяетсякогда функция объявленаОн определяется местоположением (в отличие от лексической области видимости, динамическая область действия подтверждается при выполнении функции, js не имеет динамической области видимости, но в js это очень похоже на динамическую область видимости).Лексическая область видимости создается на этапе компиляции, это набор правил доступа к идентификаторам внутри функций.В конце концов, область видимости — это просто «пустое пространство», в котором нет реальных переменных, но которые определяют правила доступа к переменным.

цепочка прицеловПо сути, это список указателей на переменные объекты, которые ссылаются только на фактические переменные объекты и не содержат их. Цепочка областей видимости определяет набор правил продолжения запроса по цепочке областей видимости, когда переменная недоступна в текущем контексте.

контекст выполнения

контекст выполнения означаетКогда функция вызываетсяОбъект переменной, сгенерированный в стеке выполнения, мы не можем получить прямой доступ к этому объекту переменной, но мы можем получить доступ к переменным,thisобъект и т. д. Например:

let fn, bar; // 1、进入全局上下文环境
bar = function(x) {
  let b = 5;
  fn(x + b); // 3、进入fn函数上下文环境
};
fn = function(y) {
  let c = 5;
  console.log(y + c); //4、fn出栈,bar出栈
};
bar(10); // 2、进入bar函数上下文环境

При каждом вызове функции в верхней части стека выполнения будет генерироваться новый контекст выполнения, и движок JavaScript будет обрабатывать их в виде стека, который мы называем стеком вызова функции (стеком вызовов). Нижняя часть стека всегда является глобальным контекстом, а вершина стека — это текущий активный исполняемый контекст, также известный как активный объект (текущий контекст выполнения, синий блок на рисунке), который отличается от приостановленной переменной. объект внизу (контекст выполнения).

Разница: область действия — это набор правил доступа для идентификаторов внутри функции, которые определяются при объявлении функции, а контекст выполнения — это среда для ряда переменных, генерируемых при выполнении функции. Один генерируется, когда он определен, а другой генерируется, когда он выполняется.

понимать выполнение функции

Процесс выполнения функции разделен на две части, одна часть используется для генерации контекста выполнения, определения точки этого, объявления переменных и генерации цепочки областей видимости; другая часть предназначена для выполнения кода построчно по порядку.

  • Установление фазы контекста выполнения (происходит при вызове функции && до выполнения кода в теле функции)
  1. Генерировать объекты переменных, порядок: создать объект аргументов --> создать объявление функциональной функции --> создать объявление переменной var
  2. Создать цепочку областей действия
  3. Определите точку этого
  • этап выполнения функции
  1. Выполнять код строка за строкой последовательно, выполнять присваивание переменной при встрече с операцией присваивания, выполнять вызов ссылки на функцию при встрече с вызовом функции, выполнять условное суждение и вычисление выражения при встрече с условным суждением и выражением и т. д.

это указывает на

let fn = function(){
  alert(this.name)
}
let obj = {
  name: '',
  fn
}
fn() // 方法1
obj.fn() // 方法2
fn.call(obj) // 方法3
let instance = new fn() // 方法4
  1. Вызов функции непосредственно в методе 1fn(), такой способ вызова похож на голого командира,thisнаправлениеwindow(в строгом режиме даundefined).
  2. Метод 2 — точечный вызовobj.fn(),В настоящее времяthisнаправлениеobjобъект. точка вызоваthisОтносится к объекту, предшествующему точке.
  3. Использование в методе 3callфункция поставитьfnсерединаthisуказывает на первый параметр, вотobj. то есть использоватьcall,apply,bindфункция можетthisПеременная указывает на первый параметр.
  4. используется в методе 4newсоздал экземпляр объектаinstance, тогдаfnсерединаthisуказывает на экземплярinstance.

Что делать, если несколько правил выполняются одновременно? На самом деле приоритет вышеперечисленных четырех правил возрастает:

fn() < obj.fn() < fn.call(obj) < new fn()

первый,newВызов имеет наивысший приоритет, пока естьnewключевые слова,thisуказывает на сам экземпляр; тогда, если неnewключевые слова, естьcall、apply、bindфункция, тоthisОн указывает на первый параметр, если нетnew、call、apply、bind,Толькоobj.foo()Этот метод вызова точки,thisНаведите на объект перед точкой, наконец, полированный командирfoo()Этот способ вызова,thisнаправлениеwindow(в строгом режиме даundefined).

В es6 были добавлены стрелочные функции, и самая большая особенность стрелочных функций заключается в том, что у них нет собственныхthis、arguments、super、new.target, а стрелочная функция не имеет объекта-прототипаprototypeнельзя использовать в качестве конструктора (newФункция стрелки сообщит об ошибке).потому что у меня нет своегоthis, поэтому в стрелочной функцииthisНа самом деле, это относится к содержащей функцииthis. Будь то точечный вызов илиcallвызов, не может изменить функцию стрелкиthis.

es6 также добавляет новые модули. Среди модулей es6 самый верхний уровеньthisнаправлениеundefined, а не глобальный объект.

Закрытие

Долгое время я придерживался поверхностного понимания замыканий как «функции, определенной внутри функции». На самом деле это лишь одно из необходимых условий образования замыкания. Только когда я прочитал определение замыкания в первом томе Кайла «JavaScript, которого вы не знаете», я внезапно понял:

Закрытие происходит, когда функция может запомнить и получить доступ к лексической области видимости, в которой она находится.

let single = (function(){
  let count = 0
  return {
    plus(){
      count++
      return count
    },
    minus(){
      count--
      return count
    }
  }
})()
single.plus() // 1
single.minus() // 0

Это одноэлементный шаблон, этот шаблон возвращает объект и присваивает его переменнойsingle,Переменнаяsingleсодержит две функцииplusа такжеminus, и обе функции используют переменные в своей лексической области видимости.count. при нормальных обстоятельствахcountИ где контекст выполнения будет уничтожен в конце выполнения функции, а потому чтоcountвсе еще используется внешней средой, поэтому в конце выполнения функцииcountИ контекст выполнения, в котором он находится, не будет уничтожен, что приведет к замыканию. каждый звонокsingle.plus()илиsingle.minus(), это изменитcountПеременные изменяются, и эти две функции поддерживают ссылку на лексическую область видимости, в которой они находятся.

Замыкания — это на самом деле особый вид функций, которые могут обращаться к переменным внутри функции, а также сохранять значения этих переменных в памяти и не очищаться механизмом сборки мусора после вызова функции.

Взгляните на классический Amway:

// 方法1
for (var i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}
// 方法2
for (let i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}

В методе 1 в цикле задаются пять таймеров, через одну секунду будет выполнена функция обратного вызова в таймере и будет напечатана переменная.iценность . Излишне говорить, что секундой позжеiувеличилось до 5, поэтому таймер печатает 5 пять раз. (Переменная в текущей области не найдена в таймереi, поэтому по цепочке областей видимости в глобальной области видимостиi)

Способ 2, из-за es6letсоздает локальную область, поэтому цикл устанавливает пять областей, а переменные в пяти областяхiРаспределение 1-5, и в каждой области установлен таймер, и переменная печатается на одну секунду позжеiценность . Через одну секунду таймеры находят переменные из соответствующих родительских областей.iсоставляет 1-5. Это новый способ использования замыканий для разрешения исключений переменных в циклах.

Прототипы и цепочки прототипов

Объекты в js создаются конструкторами (литералы объектов на самом деле являются синтаксическим сахаром и также создаются конструкторами по сути). За исключением стрелочных функций, все функции имеют функцию с именемprototypeхарактеристики. js встроенные функции все вprototypeСуществует множество методов, определенных наArray.prototypeизslice splice join split filter reduceИ т. д. и т. д.

Почти все объекты в js имеют специальный[[Prototype]]Встроенное свойство, используемое для указания объекта-прототипа объекта, это свойство по сути является ссылкой на другие объекты. Частная собственность обычно отображается в браузере.__proto__, по факту[[Prototype]]реализация браузера. Сам объект имеет встроенный[[Prototype]]Указывает на объект-прототип, который также имеет свой собственный[[Prototype]]Указание на другие объекты-прототипы, которые соединены последовательно, образуя цепочку прототипов.

const arr = [1, 2, 3]
arr.__proto__ === Array.prototype // true
Array.prototype.__proto__ === Object.prototype // true
Object.prototype.__proto__ === null // true

Видно, что в приведенном выше примере естьarrприбытьnullЦепочка прототипов выглядит следующим образом:

arr----__proto__---->Array.prototype----__proto__---->Object.prototype----__proto__---->null

переменнаяarrможет получить доступAarray.prototypeа такжеObject.prototypeспособ выше.

Цепочка прототипов по-прежнему является сутью реализации наследования в js, которая будет обсуждаться в следующем разделе.

Выше я сказал "в jsПочти всеобъекты имеют особое[[Prototype]]встроенные свойства», почему не все из них? Потому что js не может создавать встроенные свойства[[Prototype]]Объект:

var o = Object.create(null)
o.__proto__ // undefined

Object.createэто метод es5, который уже поддерживается всеми браузерами. Этот метод создает и возвращает новый объект и присваивает объект-прототип нового объекта в качестве первого параметра. В приведенном выше примереObject.create(null)Создано свойство, не имеющее встроенных свойств.[[Prototype]]новый объект.

js наследование

Наследование js реализовано через цепочку прототипов, за подробностями обращайтесь к моемуэта статья, здесь я говорю только о "поведенческом делегировании", с которым вы, возможно, незнакомы. Делегирование поведения — это метод, рекомендованный Кайлом, автором серии статей «JavaScript, которого вы не знаете».setPrototypeOfМетод [[Protytype]] связан с построенным объектом-прототипом другого объекта для достижения цели наследования.

let SuperType = {
  initSuper(name) {
    this.name = name
    this.color = [1,2,3]
  },
  sayName() {
    alert(this.name)
  }
}
let SubType = {
  initSub(age) {
    this.age = age
  },
  sayAge() {
    alert(this.age)
  }
}
Object.setPrototypeOf(SubType,SuperType)
// 此时 SubType.__proto__ === SuperType
SubType.initSub('17')
SubType.initSuper('gim')
SubType.sayAge() // 'gim'
SubType.sayName() // '17'

В приведенном выше примере нужно поместить родительский объектSuperTypeСвязать с дочерним объектомSubTypeвстроенный прототип, так что методы родительского объекта можно вызывать непосредственно в дочернем объекте. Цепочка прототипов, созданная путем делегирования поведения, проще и понятнее, чем цепочка прототипов, созданная путем наследования классов, и она понятна с первого взгляда.

行为委托

event loop

js является однопоточным, все задачи нужно ставить в очередь, а следующая задача будет выполняться после завершения предыдущей задачи. Если первая задача занимает много времени, вторая задача должна ждать вечно. Однако IO-устройства (устройства ввода и вывода) очень медленные (например, Ajax-операции для чтения данных из сети), и js не может дождаться завершения IO-устройств, прежде чем продолжить выполнение следующей задачи, что теряет смысл этого язык. Таким образом, задачи js делятся на синхронные задачи и асинхронные задачи.

  1. Все задачи синхронизации выполняются в основном потоке, образуя «стек контекста выполнения»;
  2. Все асинхронные задачи будут временно приостановлены, а дождавшись результата операции, его функция обратного вызова войдет в «очередь задач» (task queue) для ожидания в очереди;
  3. Когда все задачи синхронизации в стеке выполнения будут завершены, будет прочитана первая функция обратного вызова в очереди задач, и функция обратного вызова будет помещена в стек выполнения, чтобы начать выполнение;
  4. Основной поток продолжает повторять третий шаг в цикле, который является рабочим механизмом «цикла событий».

На приведенном выше рисунке при выполнении основного потока генерируются куча и стек. Куча используется для хранения ссылочных типов, таких как объекты массива. Код в стеке вызывает различные внешние API, которые добавляются в "задачу". очередь». события (щелчок, загрузка, выполнение). Пока код в стеке выполняется, основной поток будет читать «очередь задач» и по очереди выполнять функции обратного вызова, соответствующие этим событиям.

В очереди задач есть два вида асинхронных задач, одна из которых представляет собой задачу макроса, в том числеscript setTimeout setIntervalи т. д., другой — микрозадачи, в том числеPromise process.nextTick MutationObserverЖдать. Всякий раз, когда запускается js-скрипт, он будет выполняться первымscriptКогда задача синхронизации в стеке выполнения будет завершена, первая задача в микрозадаче будет выполнена и помещена в стек выполнения для выполнения.Когда стек выполнения пуст, микрозадача будет прочитана и выполнена снова, и цикл повторится пока Список микрозадач не опустеет. Когда список микрозадач пуст, первая задача в макрозадаче будет прочитана и помещена в стек выполнения для выполнения. микрозадача пуста и так далее.