В последнее время автор планомерно перебирает знания о нативном JS, потому что я считаю, что JS, как фундаментальную технологию фронтенд-инженеров, невозможно переоценить. Я планирую сделать серию, которая будет разделена на три раздачи в общей сложности, по ряду вопросов, и, конечно же, будет продолжение и расширение, содержание систематизировано и полно, это будет хорошим улучшением для младшие и средние игроки, а также продвинутые игроки также получат обзор и консолидацию. Пожалуйста, обратите внимание!
Часть 1: Вопросы о типах данных JS — концепции
1. Каковы примитивные типы данных JS? Что такое справочные типы данных?
В JS есть 7 примитивных значений, а именно:
- boolean
- null
- undefined
- number
- string
- symbol
- bigint
Тип справочных данных: Объект Объект (включая обычный объект-Объект, объект-массив-Массив, обычный объект-RegExp, объект даты-Дата, математическая функция-Математика, объект-функция-Функция)
2. Укажите результат следующей операции и объясните, почему.
function test(person) {
person.age = 26
person = {
name: 'hzj',
age: 18
}
return person
}
const p1 = {
name: 'fyq',
age: 19
}
const p2 = test(p1)
console.log(p1) // -> ?
console.log(p2) // -> ?
результат:
p1:{name: “fyq”, age: 26}
p2:{name: “hzj”, age: 18}
Причина: Когда функция передает параметры, передается значение адреса памяти объекта в куче. Фактический параметр person в тестовой функции — это адрес памяти объекта p1. Значение p1 действительно изменяется при вызове person.age = 26, но затем человек становится адресом другого пространства памяти и, наконец, возвращает адрес этого другого пространства памяти и присваивает его p2.
3. Является ли null объектом? Почему?
Вывод: null не является объектом.
Объяснение: Хотя typeof null выводит объект, это просто давняя ошибка в JS. В исходной версии JS использовалась 32-битная система.Из соображений производительности информация о типе переменной хранится в младших битах.Начало 000 представляет объект, но ноль представляет все нули, поэтому оценивается неправильно как объект 。
4. Почему можно вызвать '1'.toString()?
На самом деле в процессе выполнения этого оператора выполняются следующие действия:
var s = new Object('1');
s.toString();
s = null;
Шаг 1: Создайте экземпляр класса Object. Обратите внимание, почему не String? В связи с появлением Symbol и BigInt вызов для них new будет приводить к ошибке.В настоящее время спецификация ES6 не рекомендует использовать new для создания классов-оболочек для базовых типов.
Шаг второй: вызов метода экземпляра.
Шаг 3: Уничтожьте экземпляр сразу после выполнения метода.
Весь процесс отражает基本包装类型
Природа и основные типы упаковки относятся к базовым типам данных, включая Boolean, Number и String.
Ссылка: «Расширенное программирование на JavaScript (третье издание)», стр. 118.
Почему 5.0.1+0.2 не равно 0.3?
0,1 и 0,2 будут бесконечно зацикливаться после преобразования в двоичное. Из-за ограничения стандартных цифр лишние цифры будут усечены. В это время уже произошла потеря точности. Двоичное число становится 0,3000000000000000004 при преобразовании в десятичное.
6. Как понять BigInt?
Что такое БигИнт?
BigInt — это новый тип данных, используемый, когда целочисленное значение превышает диапазон, поддерживаемый типом данных Number. Этот тип данных позволяет нам безопасно
大整数
Выполняйте арифметические операции, представляйте временные метки с высоким разрешением, используйте большие целочисленные идентификаторы и т. д. без использования библиотеки.
Зачем вам BigInt?
В JS все числа представлены в 64-битном формате двойной точности с плавающей запятой, так в чем же проблема?
Это заставляет число в JS не точно представлять очень большие целые числа, оно будет округлять очень большие целые числа, если быть точным, числовой тип в JS может безопасно представлять только -9007199254740991(-(2^53-1)) и 9007199254740991((2^ 53-1)), любое целочисленное значение за пределами этого диапазона может потерять точность.
console.log(999999999999999); //=>10000000000000000
Есть также некоторые проблемы с безопасностью:
9007199254740992 === 9007199254740993; // → true 居然是true!
Как создать и использовать BigInt?
Чтобы создать BigInt, просто добавьте n в конец числа.
console.log( 9007199254740995n ); // → 9007199254740995n
console.log( 9007199254740995 ); // → 9007199254740996
Другой способ создать BigInt — использовать конструктор BigInt(),
BigInt("9007199254740995"); // → 9007199254740995n
Простое использование следующим образом:
10n + 20n; // → 30n
10n - 20n; // → -10n
+10n; // → TypeError: Cannot convert a BigInt value to a number
-10n; // → -10n
10n * 20n; // → 200n
20n / 10n; // → 2n
23n % 10n; // → 3n
10n ** 3n; // → 1000n
const x = 10n;
++x; // → 11n
--x; // → 9n
console.log(typeof x); //"bigint"
Моменты, которых следует опасаться
-
BigInt не поддерживает унарный оператор плюс, что может быть связано с тем, что некоторые программы могут полагаться на инвариант +, чтобы всегда создавать число или генерировать исключение. Кроме того, изменение поведения + также нарушает код asm.js.
-
Смешивание операций между bigint и Number не допускается, поскольку неявные преобразования типов могут привести к потере информации. При смешивании больших целых чисел и чисел с плавающей запятой результирующее значение может не быть точно представлено BigInt или Number.
10 + 10n; // → TypeError
- BigInt нельзя передать веб-API и встроенным функциям JS, которые ожидают число типа Number . Попытка сделать это вызовет TypeError.
Math.max(2n, 4n, 6n); // → TypeError
- Когда тип Boolean встречается с типом BigInt, BigInt обрабатывается как число, другими словами, BigInt рассматривается как истинное значение, если оно не равно 0n.
if(0n){//条件判断为false
}
if(3n){//条件为true
}
-
Массивы, все элементы которых являются BigInt, можно сортировать.
-
BigInt может нормально выполнять побитовые операции, такие как |, &, > и ^
Совместимость с браузером
Результат каниуса:
На самом деле, текущая совместимость не очень хороша, только массовым реализациям, таким как chrome67, firefox и Opera, предстоит пройти долгий путь, прежде чем они станут нормой.
Мы с нетерпением ждем светлого будущего для BigInt!
Часть 2: Вопросы о типах данных JS — обнаружение
1. Может ли typeof правильно определить тип?
Для примитивных типов все, кроме null, могут вызывать typeof для отображения правильного типа.
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
Но для справочных типов данных, кроме функций, отображается «объект».
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
Таким образом, не подходит использовать typeof для оценки типа данных объекта. Лучше использовать instanceof. Принцип instanceof основан на запросе цепочки прототипов. Пока он находится в цепочке прототипов, суждение всегда истинно.
const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true
var str1 = 'hello world'
str1 instanceof String // false
var str2 = new String('hello world')
str2 instanceof String // true
2. Может ли instanceof судить об основном типе данных?
могу. Например так:
class PrimitiveNumber {
static [Symbol.hasInstance](x) {
return typeof x === 'number'
}
}
console.log(111 instanceof PrimitiveNumber) // true
Если вы не знаете символ, проверьте егоОбъяснение hasInstance на MDN.
На самом деле это способ настроить поведение instanceof.Здесь исходный метод instanceof переопределяется и заменяется на typeof, так что можно судить об основном типе данных.
3. Можно ли вручную реализовать функцию instanceof?
Ядро: поиск по цепочке прототипов.
function myInstanceof(left, right) {
//基本数据类型直接返回false
if(typeof left !== 'object' || left === null) return false;
//getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(left);
while(true) {
//查找到尽头,还没找到
if(proto == null) return false;
//找到相同的原型对象
if(proto == right.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}
тестовое задание:
console.log(myInstanceof("111", String)); //false
console.log(myInstanceof(new String("111"), String));//true
4. В чем разница между Object.is и ===?
Объект исправляет некоторые ошибки особого случая, основанные на строгом равенстве, в частности, +0 и -0, NaN и NaN. Исходный код выглядит следующим образом:
function is(x, y) {
if (x === y) {
//运行到1/x === 1/y的时候x和y都为0,但是1/+0 = +Infinity, 1/-0 = -Infinity, 是不一样的
return x !== 0 || y !== 0 || 1 / x === 1 / y;
} else {
//NaN===NaN是false,这是不对的,我们在这里做一个拦截,x !== x,那么一定是 NaN, y 同理
//两个都是NaN的时候返回true
return x !== x && y !== y;
}
Часть 3: Вопросы о типах данных JS — преобразование
1. Каков результат [] == ![]? Почему?
Разобрать:
В == обе стороны должны быть преобразованы в числа, а затем сравнены.
[] преобразуется в число 0.
![] сначала преобразуется в логическое значение, так как [] как ссылочный тип преобразуется в логическое значение true,
Таким образом, ![] является ложным, что, в свою очередь, преобразуется в число и становится равным 0.
0 == 0 , результат верен
2. Какие типы преобразования типов существуют в JS?
В JS существует всего три типа преобразования типов:
- преобразовать в числа
- Преобразовать в логическое значение
- преобразовать в строку
Конкретные правила преобразования заключаются в следующем:
Обратите внимание, что результат строки «Boolean to string» относится к примеру true to string
3. В чем разница между == и ===?
===叫做严格相等,是指:左右两边不仅值要相等,类型也要相等,例如'1'===1的结果是false,因为一边是string,另一边是number。
== не так строго, как ===.В общем случае, пока значения равны, он возвращает true, но == также включает некоторые преобразования типов, и его правила преобразования следующие:
- Одинаковы ли типы с обеих сторон, если они одинаковы, сравните размер значения, например 1 == 2, верните false
- Судя по тому, является ли он нулевым и неопределенным, если да, верните true
- Является ли оцениваемый тип строкой и числом, если да, преобразуйте тип строки в число, а затем сравните
- Определите, является ли одна из сторон логическим значением, если да, преобразуйте логическое значение в число, а затем сравните
- Если один из них является объектом, а другой — строкой, числом или символом, объект будет преобразован в строку, а затем сравнен
console.log({a: 1} == true);//false
console.log({a: 1} == "[object Object]");//true
4. Как происходит преобразование объектов в примитивные типы?
При преобразовании объекта к примитивному типу будет вызвана встроенная функция [ToPrimitive], для которой логика следующая:
- Если метод Symbol.toPrimitive(), сначала вызовите его, а затем верните
- вызов valueOf(), который возвращает значение при приведении к примитивному типу
- Вызов toString(), который возвращает значение при приведении к примитивному типу
- Если ни один из исходных типов не будет возвращен, будет сообщено об ошибке
var obj = {
value: 3,
valueOf() {
return 4;
},
toString() {
return '5'
},
[Symbol.toPrimitive]() {
return 6
}
}
console.log(obj + 1); // 输出7
5. Как сделать условие if(a == 1 && a == 2) истинным?
По сути, это приложение к предыдущему вопросу.
var a = {
value: 0,
valueOf: function() {
this.value++;
return this.value;
}
};
console.log(a == 1 && a == 2);//true
Часть 4. Расскажите о своем понимании замыканий
Что такое закрытие?
红宝书(p178)上对于闭包的定义:闭包是指有权访问另外一个函数作用域中的变量的函数,
MDN 对闭包的定义为:闭包是指那些能够访问自由变量的函数。
(其中自由变量,指在函数中使用的,但既不是函数参数arguments也不是函数的局部变量的变量,其实就是另外一个函数作用域中的变量。)
Причина закрытия?
Прежде всего, мы должны понять концепцию цепочки областей видимости.На самом деле она очень проста.В ES5 есть только две области видимости-глобальная область видимости и область действия функции.当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,就去父作用域找,直到找到该变量的标示符或者不在父作用域中,这就是作用域链
, Стоит отметить, что каждая подфункция будет копировать область действия верхнего уровня, чтобы сформировать цепочку областей действия. Например:
var a = 1;
function f1() {
var a = 2
function f2() {
var a = 3;
console.log(a);//3
}
}
В этом коде область f1 указывает на глобальную область (окно) и себя, а область действия f2 указывает на глобальную область (окно), f1 и себя. И поиск области осуществляется снизу вверх до тех пор, пока не будет найдено окно глобальной области видимости.Если глобальная область не существует, будет сообщено об ошибке. Это так просто!
Суть замыкания в том, что есть ссылка на родительскую область видимости в текущей среде. Или возьмем приведенный выше пример:
function f1() {
var a = 2
function f2() {
console.log(a);//2
}
return f2;
}
var x = f1();
x();
Здесь x получит переменную в родительской области и выведет 2. Потому что в текущей среде есть ссылка на f2, а f2 просто ссылается на область окна, f1 и f2. Таким образом, f2 может обращаться к переменным в области видимости f1.
Разве только функция возврата может рассматриваться как замыкание? ,
Возвращаясь к сути замыканий, нам нужно только сделать ссылку на родительскую область видимости, поэтому мы также можем сделать это:
var f3;
function f1() {
var a = 2
f3 = function() {
console.log(a);
}
}
f1();
f3();
Пусть f1 выполняется, и после присвоения значения f3 это эквивалентно тому, что теперьf3拥有了window、f1和f3本身这几个作用域的访问权限
, или поиск снизу вверх,最近是在f1
a находится в , поэтому выводится 2.
вот внешняя переменнаяf3存在着父级作用域的引用
, значит замыкание сгенерировано, форма изменилась, но суть не изменилась.
Каковы проявления замыканий?
Разобравшись в сути, давайте посмотрим, в какой реальной сцене может отразиться наличие замыкания?
- Вернуть функцию. Только что был приведен пример.
- Передано как параметр функции
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这就是闭包
fn();
}
// 输出2,而不是1
foo();
- В таймерах, обработчиках событий, Ajax-запросах, межоконной связи, Web Workers или чем-либо асинхронном всякий раз, когда вы используете обратные вызовы, вы на самом деле используете замыкания.
Следующие закрытия сохраняют только окно и текущую область.
// 定时器
setTimeout(function timeHandler(){
console.log('111');
},100)
// 事件监听
$('#app').click(function(){
console.log('DOM Listener');
})
- IIFE (Immediate Execute Function Expression) создает замыкание, сохраняет
全局作用域window
а также当前函数的作用域
, так что это может быть глобальная переменная.
var a = 2;
(function IIFE(){
// 输出2
console.log(a);
})();
Как я могу решить проблему вывода цикла ниже?
for(var i = 1; i <= 5; i ++){
setTimeout(function timer(){
console.log(i)
}, 0)
}
Почему все 6 выводятся? Как его улучшить, чтобы он выдавал 1, 2, 3, 4, 5? (чем больше методов, тем лучше)
Так как setTimeout является макрозадачей, из-за однопоточного механизма eventLoop в JS, макрозадача выполняется после выполнения основной задачи синхронизации потока, поэтому обратные вызовы в setTimeout выполняются последовательно после завершения цикла, но текущая область недоступна, когда выводится я. Поднимитесь на один уровень и найдите я. В это время цикл закончился, и я стал 6. Таким образом, все 6 будут выведены.
Решение:
1. Используйте IIFE (немедленное выполнение функционального выражения), чтобы передавать переменную i в это время таймеру каждый раз, когда цикл for
for(var i = 1;i <= 5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, 0)
})(i)
}
2. Передать третий параметр таймеру в качестве первого функционального параметра функции таймера.
for(var i=1;i<=5;i++){
setTimeout(function timer(j){
console.log(j)
}, 0, i)
}
3. Использование let в ES6
for(let i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i)
},0)
}
let произвел революцию в JS, сделав область функций JS областью на уровне блоков, а цепочка областей видимости больше не существует после использования let. Объем кода указан в единицах на уровне блоков.В качестве примера возьмем приведенный выше код:
// i = 1
{
setTimeout(function timer(){
console.log(1)
},0)
}
// i = 2
{
setTimeout(function timer(){
console.log(2)
},0)
}
// i = 3
...
Таким образом, может быть выведен правильный результат.
Часть 5. Расскажите о своем понимании цепочки прототипов.
1. Какова связь между объектами-прототипами и конструкторами?
В JavaScript всякий раз, когда определяется тип данных функции (обычная функция, класс), он рождается со свойством-прототипом, которое указывает на объект-прототип функции.
Когда функция вызывается с new, функция становится конструктором, возвращая совершенно новый объект экземпляра, который имеет атрибут __proto__, указывающий на объект-прототип конструктора.
2. Можете ли вы описать цепочку прототипов?
Объект JavaScript указывает на объект родительского класса через __proto__ до тех пор, пока он не укажет на объект Object, формируя таким образом цепочку точек прототипа, то есть цепочку прототипов.
- hasOwnProperty() объекта, чтобы проверить, содержит ли сам объект свойство
- При использовании in для проверки того, содержит ли объект определенное свойство, он также вернет true, если объект не имеет его, но имеет в цепочке прототипов.
Часть 6: Как JS реализует наследование?
Первый: с помощью звонка
function Parent1(){
this.name = 'parent1';
}
function Child1(){
Parent1.call(this);
this.type = 'child1'
}
console.log(new Child1);
При таком написании, хотя подкласс может получить значение атрибута родительского класса, проблема заключается в том, что если в объекте-прототипе родительского класса есть метод, подкласс не может его наследовать. Тогда приведите к следующему способу.
Второй: с помощью цепочки прототипов
function Parent2() {
this.name = 'parent2';
this.play = [1, 2, 3]
}
function Child2() {
this.type = 'child2';
}
Child2.prototype = new Parent2();
console.log(new Child2());
Вроде бы проблем нет, доступ к методам и свойствам родительского класса есть, но на самом деле есть потенциальный недостаток. Например:
var s1 = new Child2();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play);
Вы можете увидеть консоль:
Очевидно, я изменил только атрибут воспроизведения s1, почему также изменился s2? Просто, потому что оба экземпляра используют один и тот же объект-прототип.
Так есть ли лучший способ?
Третий: объединить первые два
function Parent3 () {
this.name = 'parent3';
this.play = [1, 2, 3];
}
function Child3() {
Parent3.call(this);
this.type = 'child3';
}
Child3.prototype = new Parent3();
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);
Вы можете увидеть консоль:
Все предыдущие вопросы были решены. Но вот новая проблема, то есть конструктор Parent3 выполнится еще раз (Child3.prototype = new Parent3();). Это то, чего мы не хотим видеть. Итак, как решить эту проблему?
Четвертое: оптимизация комбинаторного наследования 1
function Parent4 () {
this.name = 'parent4';
this.play = [1, 2, 3];
}
function Child4() {
Parent4.call(this);
this.type = 'child4';
}
Child4.prototype = Parent4.prototype;
Здесь давайте передадим объект-прототип родительского класса непосредственно дочернему классу, конструктор родительского класса выполняется только один раз, и доступ к свойствам и методам родительского класса возможен, но давайте проверим это:
var s3 = new Child4();
var s4 = new Child4();
console.log(s3)
Конструктором экземпляра подкласса является Parent4, что явно неверно, он должен быть Child4.
Пятое (наиболее рекомендуемое): оптимизация наследования композиции 1
function Parent5 () {
this.name = 'parent5';
this.play = [1, 2, 3];
}
function Child5() {
Parent5.call(this);
this.type = 'child5';
}
Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;
Это наиболее рекомендуемый способ, близкий к идеальному наследованию, и его также называют паразитным композиционным наследованием.
ES6 расширяет скомпилированный код JavaScript
В конце концов, код ES6 должен иметь возможность запускаться в браузере.В середине используется инструмент компиляции babel для компиляции кода ES6 в ES5, чтобы некоторые браузеры, которые не поддерживают новый синтаксис, также могли работать.
Так как же выглядит окончательная компиляция?
function _possibleConstructorReturn(self, call) {
// ...
return call && (typeof call === 'object' || typeof call === 'function') ? call : self;
}
function _inherits(subClass, superClass) {
// ...
//看到没有
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
var Parent = function Parent() {
// 验证是否是 Parent 构造出来的 this
_classCallCheck(this, Parent);
};
var Child = (function (_Parent) {
_inherits(Child, _Parent);
function Child() {
_classCallCheck(this, Child);
return _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).apply(this, arguments));
}
return Child;
}(Parent));
Ядром является функция _inherits, и видно, что она по-прежнему использует пятый метод, метод паразитарного комбинированного наследования, что доказывает успех этого метода. Однако здесь добавляется Object.setPrototypeOf(subClass, superClass) Для чего это используется?
Ответ - это статический метод, используемый для наследует родительский класс. Это также, где метод первоначального наследования пренебрегал.
追问: 面向对象的设计一定是好的设计吗?
неуверенный. С точки зрения наследования, эта конструкция таит в себе огромные скрытые опасности.
Говоря о проблеме наследования от дизайн-мышления
Если есть машины разных марок, у каждой машины есть три метода: драйв, музыка и доливка масла.
class Car{
constructor(id) {
this.id = id;
}
drive(){
console.log("wuwuwu!");
}
music(){
console.log("lalala!")
}
addOil(){
console.log("哦哟!")
}
}
class otherCar extends Car{}
Теперь можно реализовать функцию автомобиля, и это можно использовать для расширения различных автомобилей.
Но тут возникает проблема, транспортное средство на новой энергии – это тоже автомобиль, но ему не нужно доливать масло (заправлять).
Если новый класс энергетического транспортного средства наследует автомобиль, возникает также проблема, широко известная как проблема «гориллы и банана». У гориллы в руке банан, а мне сейчас нужен только банан, а у меня горилла. То есть, да ладно, мне этот метод сейчас не нужен, но из-за наследования он тоже отдан подклассам.
Самая большая проблема с наследованием заключается в том, что невозможно решить, какие свойства наследовать, все свойства должны наследоваться.
Конечно, вы можете сказать, что можно создать еще один родительский класс и убрать метод дозаправки, но это тоже проблематично.С одной стороны, родительский класс не может описать детали всех подклассов.Для различных характеристик подкласса перейдите в Добавить разные родительские классы,代码势必会大量重复
, с другой стороны, после изменения подкласса родительский класс также должен быть соответствующим образом обновлен.代码的耦合性太高
, техническое обслуживание не является хорошим.
Так как же решить многие проблемы наследования?
Комбинация также является тенденцией сегодняшнего развития синтаксиса программирования.Например, golang полностью использует метод проектирования, ориентированный на комбинацию.
Как следует из названия, ориентированная композиция заключается в том, чтобы сначала спроектировать ряд частей, а затем собрать эти части для формирования различных экземпляров или классов.
function drive(){
console.log("wuwuwu!");
}
function music(){
console.log("lalala!")
}
function addOil(){
console.log("哦哟!")
}
let car = compose(drive, music, addOil);
let newEnergyCar = compose(drive, music);
Код чистый и многоразовый. Вот как работает композиционно-ориентированный дизайн.
Источник ссылки:
Реализация ES5 наследует эти вещи
Повторное изучение серии JS: разговор о наследовании