文章目录
- 1. 如何判断是数组
- 2. JS数据类型和相关问题
- 3. 如何避免一个对象的属性被修改?
- 4. new String('a')和'a'相同吗?
- 5. 判断数据类型
- 6. new 一个构造函数,如果函数返回 return {} 、 return null , return 1 , return true 会发生什么情况?
- 7. Symbol
- 8. 如何让 var [a, b] = {a: 1, b: 2} 解构赋值成功?
- 9. ['1','5','11'].map(parseInt) 为[1,NaN,3]['1','2','3'].map(parseInt) 为[1, NaN, NaN]
- 10. 数组去重
- 11. script标签中defer和async的区别?
- 12. 为什么 0.1 + 0.2 不等于 0.3?为什么 0.1 + 0.1 等于 0.2 ?
- 13. 准确的倒计时
- 14. let const var区别
1. 如何判断是数组
- Array.isArray
console.log(Array.isArray(arr))
- instanceof(instanceof 运算符用于验证构造函数的 prototype 属性是否出现在对象的原型链中的任意位置)
console.log(arr1 instanceof Array)
- constructor(实例的构造函数属性constructor指向构造函数)
console.log(arr.constructor === Array)
- Object.prototype.toString.call()
Object.prototype.toString.call(arr) === '[object Array]');
- 原型链
arr.__proto__ === Array.prototype
- Array.prototype.isPrototypeOf(isPrototypeOf用于判断 一个对象是否是另一个对象的原型)
Array.prototype.isPrototypeOf(arr)
2. JS数据类型和相关问题
-
数据类型
string、boolean、number、null、undefined、bigInt、symbol、Object(array、function) -
null和undefined的区别
undefined:未定义的值 。这个值的语义是,希望表示一个变量最原始的状态,而非人为操作的结果 。- 声明一个变量,但是没有赋值
- 访问对象上不存在的属性或者未定义的变量
- 函数定义了形参,但没有传递实参
- 函数没有返回值时,默认返回undefined
- 使用void对表达式求值
null:空值。这个值的语义是,希望表示一个对象被人为的重置为空对象,而非一个变量最原始的状态 。 在内存里的表示就是,栈中的变量没有指向堆中的内存对象。
- 定义的变量在将来用于保存对象,那么最好将该变量初始化为null
- 当一个数据不再需要使用时,我们最好通过将其值设置为null来解除引用
特殊的typeof null
null值表示一个空对象指针,它代表的其实就是一个空对象。 -
number和bigInt的区别
number:
JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于2的1024次方的数值,JavaScript 无法表示,会返回Infinity。
Number在计算机中采取了IEEE754 双精度规范
使用8个字节(64位进行存储),结构如下
符号位 Sign(S) : 1bit (b63)
指数部分Exponent(E) : 11bit (b62-b52)
尾数部分Mantissa(M) : 52bit (b51-b0)
表达式 -1^S * 2^E * 1.M
最大安全整数:Number.MAX_SAFE_INTEGER
=== 2^53-1
超出安全数,Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 // true
,2^53
也可以表示,但与2^53+1
相同,所以无法知道表示的是谁,2^53不安全。
最大数:Number.MAX_VALUE = 2^1024
Number.EPSILON: 是 JS 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。JS中所有数字都是浮点数,计算不精准,所以通过这个极小值,当误差小于这个值,则可以忽略不计。
bigInt:表示大整数,BigInt 类型的数据必须添加后缀n。没有位数的限制。
12n === 12
是false,类型不同,12n == 12
为true
- 不能用 Math 对象中的方法。
- 不能和 Number 类型数值进行混合算术运算,必须转换成同一类型。
- 虽然 BigInt 不能和 Number 直接进行算术运算,但使用比较运算符(==、<、>等等)直接比较是允许的。
- BigInt 转换为 Number 可能会丢失精度。
- symbol
Symbol,表示独一无二的值
当symbol作为属性名时,需要通过Object.getOwnPropertySymbols()遍历,对象的其他方法都无法遍历。
Symbol.for()可以新建两个一样的symbol
let a1 = Symbol.for('a')
let a2 = Symbol.for('a')
console.log(a1 === a2) // true
Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。
3. 如何避免一个对象的属性被修改?
在 JavaScript 中,你可以使用不同的方式来设置对象的属性为不可修改。以下是其中三种常用的方法:
- 使用 Object.defineProperty() 或 Object.defineProperties() 方法:这两个方法可以用来定义或修改对象的属性,并且可以通过设置属性描述符的相关选项来控制属性的特性。通过将 writable 设置为 false,你可以将属性设置为不可修改。
const obj = {};
Object.defineProperty(obj, 'propertyName', {
value: 'propertyValue',
writable: false, // 将属性设置为不可修改
});
obj.propertyName = 'newValue'; // 尝试修改属性,但不生效
console.log(obj.propertyName); // 输出: propertyValue
- 使用 Object.freeze() 方法:Object.freeze() 方法可以冻结一个对象,使其属性变为不可修改(包括值和属性的可配置性)。一旦对象被冻结,任何对其属性进行修改的尝试都将被忽略。
const obj = {
propertyName: 'propertyValue'
};
Object.freeze(obj); // 冻结对象
obj.propertyName = 'newValue'; // 尝试修改属性,但不生效
console.log(obj.propertyName); // 输出: propertyValue
- 使用 ECMAScript 6 的类和 get 访问器:在类中定义属性时,可以使用 get 访问器而不提供 set 访问器。这会使该属性成为只读属性,不可修改。
class MyClass {
constructor() {
this._propertyName = 'propertyValue';
}
get propertyName() {
return this._propertyName;
}
}
const obj = new MyClass();
obj.propertyName = 'newValue'; // 尝试修改属性,但不生效
console.log(obj.propertyName); // 输出: propertyValue
4. new String(‘a’)和’a’相同吗?
var str = 'hello world'
var str1 = String('hello world')
var str2 = new String('hello world')
console.log(str1 === str) // true
console.log(str2 === str) // false
str和str1都是基本类型,但str2,此时的String为一个构造函数,而 new 操作符创建了一个字符串对象,此时的 str2 为字符串对象,类型为引用类型。
5. 判断数据类型
function typeFunc(val) {
if (typeof val !== 'object') return typeof val;
else {
return Object.prototype.toString.call(val).slice(8, -1).toLocaleLowerCase();
}
}
console.log(typeFunc(new Date())) // date
6. new 一个构造函数,如果函数返回 return {} 、 return null , return 1 , return true 会发生什么情况?
- 构造函数没有返回值、原始值1、null、true,都不受影响会返回构造函数的实例
- 构造函数没有一个新的对象,那么new会生成一个新的对象
function Person() {
}
let p1 = new Person()
console.log(p1) //Person {}
function PersonNumber() {
return 1;
}
let p2 = new PersonNumber()
console.log(p2) //PersonNumber {}
function PersonObj() {
return {
name: "1234"
}
}
let p3 = new PersonObj()
console.log(p3) //{name: '1234'}
7. Symbol
- symbol运算
Symbol不能进行隐式类型转换,但可以显式转为字符串;不能转化为数字,但可以转化为布尔值
const s = Symbol('s')
// 隐式类型转换会抛类型错误
console.log(s + '/s'); // TypeError: Cannot convert a Symbol value to a string
console.log(`${s}/s`) // TypeError: Cannot convert a Symbol value to a string
// 只能先进行强制转换
console.log(String(s) + '/s'); // Symbol(s)/s
console.log(s.toString() + '/s'); // Symbol(s)/s
- 使用symbol
Symbol值作为属性名时,需要注意两点:
-
不能通过点运算符访问,需要通过方括号的形式访问。
-
不能通过for…in、for…of遍历,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是它也不是私有属性,可以通过Object.getOwnPropertySymbols()和 Reflect.ownKeys()方法获取对象Symbol 属性名。
- 方法
- Symbol.for()
它跟symbol()的区别是Symbol()定义的值每次都是新建,即使描述相同值也不相等,而Symbol.for()定义的值会先检查给定的描述是否已经存在,如果不存在才会新建一个值,并把这个值登记在全局环境中供搜索,Symbol.for()定义相同描述的值时会被搜索到,描述相同则他们就是同一个值
// Symbol.for()
let a1 = Symbol.for('a');
let a2 = Symbol.for('a');
a1 === a2 // true
// Symbol.keyFor()
let a1 = Symbol.for("a");
Symbol.keyFor(a1); // "a"
let a2 = Symbol("a");
Symbol.keyFor(a2); // undefined,Symbol()写法没有登记机制,所以每次调用都会返回一个不同的值,返回undefined
- 使用场景
- 作为对象属性 当一个复杂对象中含有多个属性的时候,很容易将某个属性名覆盖掉,利用 Symbol 值作为属性名可以很好的避免这一现象
const name = Symbol('name');
const obj = {
[name]: 'ClickPaas',
}
- ES6 中的类是没有 private 关键字来声明类的私有方法和私有变量的,但是我们可以利用 Symbol 的唯一性来模拟
const speak = Symbol();
class Person {
[speak]() {
console.log(123)
}
}
let person = new Person()
console.log(person[speak]())
8. 如何让 var [a, b] = {a: 1, b: 2} 解构赋值成功?
解构赋值:左侧是一个数组,而右侧则应该是一个具有迭代器接口的对象(如数组、Map、Set等)
var [a, b] = {a: 1, b: 2}
直接赋值会报错,因为{a: 1, b: 2}不是一个可以迭代的对象,要想赋值成功,就需要将{a: 1, b: 2}变为一个有迭代器的对象
先了解一下array为什么可以被迭代,打印数组后可以看到原型有一个方法,该方法返回一个迭代器对象
所以要想使一个数据具有迭代器,则满足
interable
{
[Symbol.iterator]: function () {
return 迭代器 (可通过next()就能读取到值)
}
}
则最终改代码如下,给object原型加一个可以迭代的对象方法即可。
Object.prototype[Symbol.iterator] = function(){
// 使用 Object.values(this) 方法获取对象的所有值,并返回数组的迭代器对象
return Object.values(this)[Symbol.iterator]()
}
var [a, b] = {a: 1, b: 2} // a,b可以正常被赋值
9. [‘1’,‘5’,‘11’].map(parseInt) 为[1,NaN,3][‘1’,‘2’,‘3’].map(parseInt) 为[1, NaN, NaN]
-
Array.map()回调函数的参数:
element:数组中当前正在处理的元素
index:正在处理的元素在数组中的索引
array:调用了 map() 的数组本身 -
parseInt()参数:
string:要被解析的值
radix:从 2 到 36 的整数,表示进制的基数。如果不再这个范围,则返回 NaN。如果是 0 ,则表示未指定,基数将会根据字符串的值进行推算(在当前场景下,会被作为 10 进制) -
['1','5','11'].map(parseInt)
为[1,NaN,3]解析:
第一次:radix 为 0 ,表示 10 进制,所以 parseInt(‘1’, 0)最后一个省略 返回 1
第二次:radix 为 1 ,不在 2-10 的范围内,所以 parseInt(‘5’, 1) 返回 NaN
第三次:radix 为 2 ,表示 2 进制,所以 parseInt(‘11’, 2) 中的 11 被作为 2 进制 ,返回的是 10 进制的结果。即:2进制的 11 转化为 10 进制为 3 -
['1','2','3'].map(parseInt)
为[1,NaN,NaN]`
10. 数组去重
不考虑数组元素类型、对象可能存在循环引用
如果对象存在循环引用,那就返回一个标识符
function uniqueArray(arr) {
const seen = new Map();
const result = [];
function isObject(item) {
return typeof item === 'object' && item !== null;
}
function stringifyWithCircular(value) {
const cache = [];
return JSON.stringify(value, (key, val) => {
if (isObject(val)) {
if (cache.includes(val)) {
// 循环引用,返回一个标识符
return '[Circular]';
}
cache.push(val);
}
return val;
});
}
for (const item of arr) {
const stringifiedItem = stringifyWithCircular(item);
console.log(stringifiedItem, 'stringifiedItem=')
if (!result.some(existingItem => stringifiedItem === stringifyWithCircular(existingItem))) {
result.push(item);
}
}
return result;
}
let obj1 = {a: 1};
let obj2 = {a: 2, self: obj1};
obj1.self = obj2;
let arr = [1,2,{a: 1}, {a: 1}, obj1, obj1, obj2]
console.log(uniqueArray(arr))
11. script标签中defer和async的区别?
css和js加载阻塞问题
脚本文件的下载和执行是与文档解析同步进行,也就是说,它会阻塞文档的解析
- async:HTML5新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。
- defer:用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。
-
script
:同步执行 。如果遇到script标签就暂停html的解析,去下载并执行script标签文件。会阻塞页面解析渲染。 -
script+async
:异步下载 同步执行。在html解析的过程中有async的script标签,就异步加载。开启新的线程,用于下载script文件。但是script文件的执行是同步的,执行的过程仍然会阻塞html的解析渲染。- 只适用于外联脚本,这一点和defer一致
- 如果有多个声明了async的脚本,其下载和执行也是异步的,不能确保彼此的先后顺序
- async会在load事件之前执行,但并不能确保与DOMContentLoaded的执行先后顺序
-
script+defer
:异步加载 延后执行。和async一样,script引用的外部文件是异步加载的,但是defer会将外部文件的执行推迟到html解析结束后再执行。
关于defer我们需要注意下面几点:- defer只适用于外联脚本,如果script标签没有指定src属性,只是内联脚本,不要使用defer
- 如果有多个声明了defer的脚本,则会按顺序下载和执行
- defer脚本会在DOMContentLoaded和load事件之前执行
12. 为什么 0.1 + 0.2 不等于 0.3?为什么 0.1 + 0.1 等于 0.2 ?
- 所以0.2+0.1因为转为2进制时丢失精度,所以不等于0.3
- 为什么 0.1 + 0.1 == 0.2 是 true
0.1转为2进制16位为0.1000000000000000,所以是0.1+0.1=0.2
因为 64 位浮点数,小数部分最多展示 16 位,因为在 IEEE 754 标准中的 64 位浮点数的小数部分,最多有 53 位, 2 的 53 次方就是 16 位数字,所以小数部分最多展示 16 位。
在 IEEE 754 标准中,浮点数的表示是有限的,而 0.1 和 0.2 在二进制下是无限循环小数。为什么是无限循环呢,这就要了解 10 进制小数如何转换为 2 进制小数了,方法如下:
“乘 2 取整,顺序排列”
- 用 2 乘十进制小数,可以得到积,将积的整数部分取出
- 再用 2 乘余下的小数部分,又得到一个积,再将积的整数部分取出
- 如此进行,直到积中的小数部分为零,或者达到所要求的精度为止
如何解决?
- 可以将其转换为整数后再进行运算,运算后再转换为对应的小数
var a = 0.1, b = 0.2
var result = (a * 100 + b * 100) / 100
console.log(result) // 0.3
console.log(result === 0.3) // true
- 利用 ES6 中的极小数 Number.EPSILON 来进行判断
var a = 0.1, b = 0.2, c = 0.3;
var result = (Math.abs(a + b - c) < Number.EPSILON);
console.log(result) // true
- 将数字转化为字符串,然后模拟加法运算
function addStrings(num1, num2) {
// 分割整数部分和小数部分
let [int1, dec1] = num1.split('.');
let [int2, dec2] = num2.split('.');
// 确保小数部分不为 undefined
dec1 = dec1 || '';
dec2 = dec2 || '';
// 让两个小数部分的长度相同
let maxDecLen = Math.max(dec1.length, dec2.length);
dec1 = dec1.padEnd(maxDecLen, '0');
dec2 = dec2.padEnd(maxDecLen, '0');
// 反转小数部分进行加法运算
let carry = 0;
let decimalResult = [];
for (let i = maxDecLen - 1; i >= 0; i--) {
let sum = parseInt(dec1[i], 10) + parseInt(dec2[i], 10) + carry;
carry = Math.floor(sum / 10);
decimalResult.push(sum % 10);
}
decimalResult = decimalResult.reverse().join('').replace(/0+$/, ''); // 去掉尾部多余的零
// 反转整数部分进行加法运算
let intResult = [];
int1 = int1.split('').reverse().join('');
int2 = int2.split('').reverse().join('');
let maxIntLen = Math.max(int1.length, int2.length);
for (let i = 0; i < maxIntLen; i++) {
let digit1 = parseInt(int1[i] || '0', 10);
let digit2 = parseInt(int2[i] || '0', 10);
let sum = digit1 + digit2 + carry;
carry = Math.floor(sum / 10);
intResult.push(sum % 10);
}
if (carry) {
intResult.push(carry);
}
intResult = intResult.reverse().join('');
// 组合整数部分和小数部分
let result = intResult;
if (decimalResult) {
result += '.' + decimalResult;
}
return result;
}
console.log(addStrings("0.1", "0.2")); // 输出: "0.3"
13. 准确的倒计时
function example5(leftTime) {
const now = performance.now();
function start() {
setTimeout(() => {
const diff = leftTime - (performance.now() - now);
console.log(diff);
if(diff > 0) {
requestAnimationFrame(start);
} else {
console.log('Time is up!');
}
}, 1000);
}
start();
}
example5(5000)
没有解决setTimeout会延迟的问题,当线程被占用之后,很容易出现误差
更精准的倒计时&计时器组件
14. let const var区别
- var 声明是全局作用域或函数作用域,而 let 和 const 是块作用域。
- var 变量可以在其作用域内更新和重新声明;let 变量可以更新但不能重新声明;const 变量既不能更新也不能重新声明。
- var 和 let 可以在不初始化的情况下声明,而 const 必须在声明时初始化
- let const 存在暂时死区,在定义之前不可以使用
- var在全局作用域声明的变量有一种行为会挂载在window对象上,let和const不会