JavaScript基础
一、数据类型
8种数据类型
最新的 ECMAScript 标准定义了 8 种数据类型:
- 七种基本数据类型:
- 布尔值(Boolean),有 2 个值分别是:true 和 false.
- null,一个表明 null 值的特殊关键字。JavaScript 是大小写敏感的,因此 null 与 Null、NULL或变体完全不同。
- undefined,和 null 一样是一个特殊的关键字,undefined 表示变量未赋值时的属性。
- 数字(Number),整数或浮点数,例如: 42 或者 3.14159。
- 字符串(String),字符串是一串表示文本值的字符序列,例如:"Howdy" 。
- 代表(Symbol)( 在 ECMAScript 6 中新添加的类型).。一种实例是唯一且不可改变的数据类型。
- 任意精度的整数 (BigInt) ,可以安全地存储和操作大整数,甚至可以超过数字的安全整数限制。
- 以及对象(Object),即引用类型。包括 Object Array、Function 等。
区别
其中 Symbol 和 BigInt 是ES6 中新增的数据类型:
- Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
- BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
这些数据可以分为原始数据类型和引用数据类型:
- 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
- 堆:引用数据类型(对象、数组和函数)
两种类型的区别在于存储位置的不同:
- 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
- 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:
- 在数据结构中,栈中数据的存取方式为先进后出。
- 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。
在操作系统中,内存被分为栈区和堆区:
- 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。
二、JS判断数据类型的方法
(1)typeof
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object
其中数组、对象、null都会被判断为object,其他判断都正确。
(2)instanceof
instanceof
可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
可以看到,instanceof
只能正确判断引用数据类型,而不能判断基本数据类型。instanceof
运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype
属性。
(3) constructor
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
constructor
有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor
对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor
就不能用来判断数据类型了:
function Fn(){};
Fn.prototype = new Array();
var f = new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
(4)Object.prototype.toString.call()
Object.prototype.toString.call()
使用 Object 对象的原型方法 toString 来判断数据类型:
var a = Object.prototype.toString;
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));
同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)
的结果不一样,这是为什么?
这是因为toString是Object的原型方法,而Array
、function
等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString
方法(function
类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。
三、判断数组的方式有哪些
- 通过Object.prototype.toString.call()做判断
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
- 通过原型链做判断
obj.__proto__ === Array.prototype;
- 通过ES6的Array.isArray()做判断
Array.isArrray(obj);
- 通过instanceof做判断
obj instanceof Array
- 通过 Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)
四、null和undefined区别
首先
Undefined
和Null
都是基本数据类型,这两个基本数据类型分别都只有一个值,就是undefined
和null
。undefined
代表的含义是未定义,null
代表的含义是空对象。一般变量声明了但还没有定义的时候会返回undefined
,null
主要用于赋值给一些可能会返回对象的变量,作为初始化。undefined
在 JavaScript 中不是一个保留字,这意味着可以使用undefined
来作为一个变量名,但是这样的做法是非常危险的,它会影响对undefined
值的判断。我们可以通过一些方法获得安全的undefined
值,比如说void 0
。当对这两种类型使用
typeof
进行判断时,Null
类型化会返回object
,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回true
,使用三个等号时会返回false
。
五、typeof null 的结果是什么
typeof null
的结果是Object。
在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签(1-3 bits) 以及当前要存储值的真实数据。类型标签存储在每个单元的低位中,共有五种数据类型:
000: object - 当前存储的数据指向一个对象。
1: int - 当前存储的数据是一个 31 位的有符号整数。
010: double - 当前存储的数据指向一个双精度的浮点数。
100: string - 当前存储的数据指向一个字符串。
110: boolean - 当前存储的数据是布尔值。
如果最低位是 1,则类型标签标志位的长度只有一位;如果最低位是 0,则类型标签标志位的长度占三位,为存储其他四种数据类型提供了额外两个 bit 的长度。
有两种特殊数据类型:
undefined
的值是 (-2)30(一个超出整数范围的数字);null
的值是机器码 NULL 指针(null 指针的值全是 0)
那也就是说null的类型标签也是000,和Object的类型标签一样,所以会被判定为Object。
六、intanceof操作符的实现原理及实现
instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
function myInstanceof(left, right) {
// 获取对象的原型
let proto = Object.getPrototypeOf(left)
// 获取构造函数的 prototype 对象
let prototype = right.prototype;
// 判断构造函数的 prototype 对象是否在对象的原型链上
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
// 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
proto = Object.getPrototypeOf(proto);
}
}
七、JavaScript 精度问题
案例
0.1 + 0.2 = 0.30000000000000004
0.07*100 = 7.000000000000001
在开发过程中遇到类似这样的问题:
let n1 = 0.1, n2 = 0.2
console.log(n1 + n2) // 0.30000000000000004
这里得到的不是想要的结果,要想等于0.3,就要把它进行转化:
(n1 + n2).toFixed(2) // 注意,toFixed为四舍五入
toFixed(num)
方法可把 Number 四舍五入为指定小数位数的数字。那为什么会出现这样的结果呢?
计算机是通过二进制的方式存储数据的,所以计算机计算0.1+0.2的时候,实际上是计算的两个数的二进制的和。0.1的二进制是0.0001100110011001100...
(1100循环),0.2的二进制是:0.00110011001100...
(1100循环),这两个数的二进制都是无限循环的数。那JavaScript是如何处理无限循环的二进制小数呢?
一般我们认为数字包括整数和小数,但是在 JavaScript 中只有一种数字类型:Number,它的实现遵循IEEE 754标准,使用64位固定长度来表示,也就是标准的double双精度浮点数。在二进制科学表示法中,双精度浮点数的小数部分最多只能保留52位,再加上前面的1,其实就是保留53位有效数字,剩余的需要舍去,遵从“0舍1入”的原则。
根据这个原则,0.1和0.2的二进制数相加,再转化为十进制数就是:0.30000000000000004
。
下面看一下双精度数是如何保存的:
- 第一部分(蓝色):用来存储符号位(sign),用来区分正负数,0表示正数,占用1位
- 第二部分(绿色):用来存储指数(exponent),占用11位
- 第三部分(红色):用来存储小数(fraction),占用52位
对于0.1,它的二进制为:
0.00011001100110011001100110011001100110011001100110011001 10011...
转为科学计数法(科学计数法的结果就是浮点数):
1.1001100110011001100110011001100110011001100110011001*2^-4
可以看出0.1的符号位为0,指数位为-4,小数位为:
1001100110011001100110011001100110011001100110011001
那么问题又来了,指数位是负数,该如何保存呢?
IEEE标准规定了一个偏移量,对于指数部分,每次都加这个偏移量进行保存,这样即使指数是负数,那么加上这个偏移量也就是正数了。由于JavaScript的数字是双精度数,这里就以双精度数为例,它的指数部分为11位,能表示的范围就是0~2047,IEEE固定双精度数的偏移量为1023。
- 当指数位不全是0也不全是1时(规格化的数值),IEEE规定,阶码计算公式为 e-Bias。 此时e最小值是1,则1-1023= -1022,e最大值是2046,则2046-1023=1023,可以看到,这种情况下取值范围是
-1022~1013
。 - 当指数位全部是0的时候(非规格化的数值),IEEE规定,阶码的计算公式为1-Bias,即1-1023= -1022。
- 当指数位全部是1的时候(特殊值),IEEE规定这个浮点数可用来表示3个特殊值,分别是正无穷,负无穷,NaN。 具体的,小数位不为0的时候表示NaN;小数位为0时,当符号位s=0时表示正无穷,s=1时候表示负无穷。
对于上面的0.1的指数位为-4,-4+1023 = 1019 转化为二进制就是:1111111011
.
所以,0.1表示为:
0 1111111011 1001100110011001100110011001100110011001100110011001
说了这么多,是时候该最开始的问题了,如何实现0.1+0.2=0.3呢?
对于这个问题,一个直接的解决方法就是设置一个误差范围,通常称为“机器精度”。对JavaScript来说,这个值通常为2-52,在ES6中,提供了Number.EPSILON
属性,而它的值就是2-52,只要判断0.1+0.2-0.3
是否小于Number.EPSILON
,如果小于,就可以判断为0.1+0.2 ===0.3
function numberepsilon(arg1,arg2){
return Math.abs(arg1 - arg2) < Number.EPSILON;
}
console.log(numberepsilon(0.1 + 0.2, 0.3)); // true
八、JavaScript脚本延迟加载的方式
延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。
一般有以下几种方式:
- defer 属性: 给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
- async 属性: 给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
- 动态创建 DOM 方式: 动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
- 使用 setTimeout 延迟方法: 设置一个定时器来延迟加载js脚本文件
- 让 JS 最后加载: 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。
九、数组去重方法
1. 使用Set 去重
将数组转化为 Set 对象,去重后再转化回数组,Set 会自动去重。
const arr = [1, 2, 3, 2, 1, 4]
const uniqueArr = [...new Set(arr)]
console.log(uniqueArr) // [1, 2, 3, 4]
- filter + indexOf
使用 filter:遍历数组,对每个元素判断是否在新数组中出现过。
indexOf 查找元素
const arr = [1, 2, 3, 2, 1, 4]
const uniqueArr = arr.filter((item, index) => {
return arr.indexOf(item) === index
})
console.log(uniqueArr) // [1, 2, 3, 4]
- 使用 Map:遍历数组,将每个元素作为 key 存储到 Map 中,去重后再转化回数组
const arr = [1, 2, 3, 2, 1, 4]
const map = new Map()
arr.forEach((item) => {
map.set(item, true)
})
const uniqueArr = Array.from(map.keys())
console.log(uniqueArr) // [1, 2, 3, 4]
- 使用对象键(和map相似) 缺点得到的key 是字符串,对于数字类型需要转化
const arr = [1, 2, 3, 2, 1, 4]
const map = {}
arr.forEach((item) => {
map[item]=true
})
const uniqueArr = Object.keys().map(item=>Number(item))
console.log(uniqueArr) // [1, 2, 3, 4]
对对象数组去重
将数据转化为JSON去重
const arr = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' },
{ id: 3, name: 'Charlie' }
];
const strArr = arr.map((item)=>JSON.stringify(item));
const result = Array.from(new Set(strArr), (item)=>JSON.parse(item)); // 将JSON数据转化为对象
console.log(result);
// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }]
这个方法的原理是:先使用 Array.prototype.map() 将数组中的对象转换为字符串,然后再用 Set 去重,最后再将字符串转换回对象。
检查数组是否含有某个值
在 JavaScript 中,有多种方法可以检查数组是否包含项目。您始终可以使用for
循环或Array.indexOf()
方法,但 ES6 添加了许多更有用的方法来搜索数组并轻松找到您要查找的内容。
indexOf() 方法
检查项目是否存在于数组中的最简单和最快的方法是使用Array.indexOf()
方法。此方法在数组中搜索给定项目并返回其索引。如果未找到任何项目,则返回 -1
。
const fruits = ['🍎', '🍋', '🍊', '🍇', '🍍', '🍐'];
fruits.indexOf('🍋'); // 1 (true)
fruits.indexOf('🍍'); // 4 (true)
fruits.indexOf('🍌'); // -1 (false)
默认情况下,该indexOf()
方法从数组的开头开始搜索,并在数组的末尾停止。但是您可以传入一个位置作为第二个参数以跳过要包含在搜索中的起始元素:
fruits.indexOf('🍋', 1); // 1 (true)
fruits.indexOf('🍋', 4); // -1 (false)
请注意,如果该项目出现多次,则该indexOf()
方法返回第一次出现的位置。
JavaScript 为我们提供了一个替代的数组方法,称为lastIndexOf()
. 顾名思义,它返回数组中项目最后一次出现的位置。在lastIndexOf()
开始搜索结束数组和数组的开头停止。您还可以指定第二个参数以在最后排除项目。
const fruits = ['🍎', '🍋', '🍊', '🍇', '🍍', '🍐'];
fruits.lastIndexOf('🍇'); // 3 (true)
fruits.lastIndexOf('🍉'); // -1 (true)
fruits.lastIndexOf('🍋', 4); // 1 (false)
无论indexOf()
和lastIndexOf()
执行在所有浏览器,包括IE9和一个区分大小写的搜索工作。
includes() 方法
该includes
方法是 ES6 的一部分,也可用于确定数组是否包含指定项。true
如果元素存在于数组中,false
则此方法返回,否则返回。该includes()
方法非常适合作为简单的布尔值查找元素是否存在。
const fruits = ['🍎', '🍋', '🍊', '🍇', '🍍', '🍐'];
fruits.includes('🍇'); // true
fruits.includes('🍉'); // false
默认情况下,该includes()
方法搜索整个数组。但是你也可以传入一个起始索引作为第二个参数来从不同的位置开始搜索:
fruits.includes('🍐', 4); // true
fruits.includes('🍊', 4); // false
除了字符串,该includes()
方法也适用于其他原始类型:
const symbol = Symbol('🌟');
const types = ['Apple', 150, null, undefined, true, 29n, symbol];
// strings
types.includes('Apple'); // true
// numbers
types.includes(150); // true
// null
types.includes(null); // true
// undefined
types.includes(undefined); // true
// boolean
types.includes(true); // true
// BigInt
types.includes(29n); // true
// Symbol
types.includes(symbol); // true
无论includes()
和indexOf()
不同的表现与NaN
(“不是一个数字”)属性:
const arr = [NaN];
// ✅
arr.includes(NaN) // true
// ❌
arr.indexOf(NaN) // -1
该incudes()
方法在 IE
中不起作用,仅在现代浏览器中可用。
find() 方法
与 不同includes()
,该find()
方法为数组中存在的每个元素执行指定的函数。它返回传递特定条件的数组中第一个元素的值:
const fruits = ['🍎', '🍋', '🍊', '🍇', '🍍', '🍐'];
const value = fruits.find(elem => elem === '🍍');
console.log(value); // 🍍
注意:在上面的例子中,我使用了一个箭头函数来循环元素。箭头函数是 ES6 的一部分,如果您不了解它们,请查看这篇文章。
如果在函数返回的位置找不到元素true
,则该find()
方法返回一个undefined
值:
const value = fruits.find(elem => elem === '🍉');
console.log(value); // undefined
您还可以获取当前元素的索引作为函数的第二个参数。当您也想比较索引时,这很有用:
fruits.find((elem, idx) => elem === '🍇' && idx > 2); // 🍇
fruits.find((elem, idx) => elem === '🍋' && idx > 2); // undefined
该find()
方法的另一个好处是它也适用于其他数据类型,如对象:
const animals = [{ name: '🐱' }, { name: '🐒' }, { whale: '🐋' }];
const found = animals.find(elem => elem.name === '🐒');
console.log(found); // { name: '🐒' }
该find()
方法仅适用于现代浏览器。
some() 方法
该some()方法的工作原理与 非常相似,find()只是true如果在数组中找到元素则返回布尔值,否则返回false.
const fruits = ['🍎', '🍋', '🍊', '🍇', '🍍', '🍐'];
fruits.some(elem => elem === '🍐'); // true
fruits.some(elem => elem === '🍓'); // false
该some()方法还可以与数组和对象一起使用:
const animals = [{ name: '🐱' }, { name: '🐒' }, { whale: '🐋' }];
animals.some(elem => elem.name === '🐒'); // true
animals.some(elem => elem.name === '🍊'); // false
您可以some()在所有现代浏览器以及 IE9 及更高版本中使用该方法。
every() 方法
该every()方法类似于,some()除了它确保数组中的所有元素都通过某个条件:
const numbers = [10, 99, 75, 45, 33];
// check if all elements are > 15
const result = numbers.every(num => num > 15);
console.log(result); // false
就像some()
,every()
适用于所有现代浏览器,以及 IE9 及更高版本。