AI摘要
这篇文章深入介绍了 JavaScript 和 ES 基础知识以及常用技巧。文章首先讨论了变量基础,包括不同的声明方式和常用数据类型。接着详细解释了变量声明、作用域、相等判断和空数据判断等内容。随后,文章介绍了对象和数组的操作方法,并讨论了常见的字符串拼接和简单类型转换技巧。此外,文章还涉及了网页定时任务、Promise、函数与箭头函数、模块化等主题,为读者提供了丰富的知识内容和实用技巧。
变量基础
JS
变量一般可以用var
,const
和let
声明,老版本可以直接赋值,不用声明。
常用类型:未定义(undefined)、空(null)、数字(Number)、字符串(String)、布尔值(Boolean)、符号(Symbol)、任意大整数(BigInt)、对象(Object),对象中有各种比较常用的数据,如函数、数组等
变量声明
-
ES5
,var
变量和函数会提升-
函数变量提升到作用域最前面
-
console.log(a); // 不会报错,但是值为:undefined b(); // 正常调用 var a = 1; function b(){ console.log(1); }
-
声明变量一定不要漏掉
var
-
c = 1; // 不允许不声明直接用,会变成全局变量,var必须
-
-
只要是支持
ES6
语法的,用const
,非必要不用let
,ESLint
可以帮助优化-
const a = 1; // 看到是const变量就知道后面不会被更改,是对象或者数组需要注意,如果对象和数组不能改:Object.freeze() let b = 1; b = 2;
-
函数声明,下面几种都可以,不过有细微区别
-
function a(){} // 函数声明,会提升到作用域最前面,可以提前调用 var b = function(){} // 声明变量,然后赋值一个匿名函数,b会提升,变量可以使用,但是是undefined,不能提前调用 const c = () => {} // ES6箭头函数,不会提升,不能提前使用变量和调用 const d = function(){} // 和前一个有细微区别,主要是函数和箭头函数的区别,后面会提到
-
注意:新项目应该都淘汰var了,不要再使用var,容易引起各种问题。
变量作用域
-
ES6
之前变量作用域,只有函数才有自己的作用域-
for(var i=0;i<10;i++){ // 执行逻辑 } console.log(i) // 输出10 (function(){ var k = 1; })() // 立即执行函数包裹之后不会有变量逃出作用域 console.log(k) // 报错
-
-
ES6
中代码块就有自己的作用域-
for(let j=0;j<10;j++){ // 执行逻辑 } console.log(j) // 报错,j不存在
-
ES6解决了很多以前必须用闭包才能解决的JS问题。
数据基础
JS
中经常需要做数据比较,比较是否相等,判断数据是否为空,如果为空不能继续支持某些操作,否则可能报错
相等判断
a === b // 判断类型
a == b // 不判断类型,1 == '1'
NaN !== NaN // 需要用isNaN()方法判断
注意:新项目一定不要用==
,开启ESlint可以帮忙验证
空数据
JS中常见的空数据:
undefined
null
0 // 字符串'0'不为空
false
NaN
''
注意JS
中{}
和[]
不是空,这种对象字面量是相当于用new
构造出来,也不相等:{}!=={}
判空代码
const a = ''
if(!a){ // 不要用a===''之类的判断
console.log('a为空:a=', a)
}
if(a){
console.log('有值:a=', a)
}
如果是简单代码一行代码,可以用&&
方法
const a = '';
a && console.log('执行操作'); // 不会执行后面的代码: console.log('执行操作')
最新链式操作,使用问号点?.
操作符可以减少大量的判断:
const a = null // 假设a是数组,类似这种结构:[{name: 'gary'}]
// const a = [{name: 'gary'}]
let b
if(a && a[0]){
b = a[0].name // 常规获取name属性
}
let c = a && a[0] && a[0].name // 骚操作获取name
let d = a?.[0]?.name // 最新链式操作
||和&&
这两个经常在判断语句中出现,其实这个表达式并不会返回Boolean
值,返回的是实际的数据
const a = 0 || 1
console.log(a) // 1
const b = 'abc' && 'def'
console.log(b) // 'def'
字符串拼接
ES6之前字符串拼接一般都是用加号,而且不支持多行
const a = 'a'
const b = a + '\n' + 'b'
ES6
有新的模板字符串语法,支持变量,而且可以换行,特别比较方便使用,尤其是拼接动态HTML
const a = 'a'
const b = `<span class="test">
${a}
</span>`
简单类型转换
const a = '1'
const b = +a // 转成数字, parseInt和parseFloat也可以,不过没有+简洁方便
const c = !!a // 转成Boolean
const d = 1
const e = ''+d // 转字符串
const f = `${d}` // 新模板字符串
正则表达式
参考:https://www.runoob.com/jsref/jsref-obj-regexp.html
正则表达式是独立于JS语言的
const re1 = new RegExp("\\w+"); // new方式新建
const re2 = /\w+/; // 字面量形式新建
const cardNo = '5122322139920319'
const cardConfusion = cardNo.replace(/^(\d{6}).*(\d{4})/, '$1******$2') // 512232******0319 分组替换
默认值
经常在开发中用到如果没有数据就给一个默认数据的情况,常规用法是用if
语句
let a = ''
if(!a){
a = '默认值'
}
代码相对来讲比较繁琐,有一些比较方便的方法。
函数默认值
如果默认值是传递给函数,ES6
函数支持默认值,默认值参数可以在中间:
function a(config={}){
}
function b(b1, b2=2, b3){
}
b(1) // b1=1, b2=2
b(1,3) // b1=1,b2=3 函数传参不能跳跃
b(1,3,4) // b1=1,b2=3,b3=4
代码中默认值
除了函数默认值之外,平时代码内部也可能用到默认值
config = config || '默认值' // 最常用默认值使用方法
config = config ?? '默认值' // 比较新的语法,老的环境可能不支持,和上面区别在于只要有值(非undefined和null)就不会用默认值
函数与箭头函数
箭头函数和函数基本可以相互替代,但是还是有不少区别
-
箭头函数没有
this
,且不能绑定this
,-
在
vue
的methods
声明方法的时候如果要用this
访问vue
实例数据就不能用箭头函数。 -
因为没有
this
,以前的apply
或call
改变this
指向的方法都没有用处。 -
const a = { b(){ console.log(this) }, c:()=>{ console.log(this) } } a.b() // a对象 a.c() // 全局window对象 const a1 = {} a.b.call(a1) // a1对象 a.c.call(a1) // 全局window对象
-
-
箭头函数没有
arguments
-
普通函数可以用
arguments
聚合参数,arguments
是伪数组,已经不推荐使用了 -
ES6
环境下可以用剩余参数
代替,剩余参数支持普通函数和箭头函数 -
function(a, b, ...args) { // 剩余参数是数组,只能在最后一个参数位置 console.log(args) }
-
-
箭头函数没有原型
-
普通函数实际上是可以作为构造函数生成对象,可以作为类使用,不是很纯粹的函数
-
箭头函数是纯粹的函数,没有原型
prototype
,不能给prototype
增加信息 -
function A(){} console.log(A.prototype) // {} console.log(new A()) // A{} const B = () => {} console.log(B.prototype) // undefined console.log(new B()) // 报错 Uncaught TypeError: B is not a constructor
-
推荐使用箭头函数,尤其是在作为回调函数的时候,如promise.then
,array.filter
,array.map
,和vue结合的时候箭头函数不会引起this指向错误
对象和数组
对象和数组是JS
中使用最频繁的数据,就是基本数据类型的组合形式
简单遍历
遍历数组
const arr = [1,2,3,4]
for(let i=0;i<arr.length;i++){
console.log(arr[i])
}
for(const item of arr){
console.log(item)
}
arr.forEach(item => console.log(item))
遍历对象
const obj = {a:1,b:2,c:3}
for(const key in obj){
console.log(`${key}: ${obj[key]}`)
}
for(const key of Object.keys(obj)){
console.log(`${key}: ${obj[key]}`)
}
for(const [key,value] of Object.entries(obj)){
console.log(`${key}: ${value}`)
}
对象合并
如果是简单的对象合并,合并后对象时原始对象的浅层拷贝,方法比较多
展开运算
ES6中支持展开运算,很方便构造新的对象
const a = {a:1,b:2}
const b = {b:3}
const c = {...a, ...b, a: 3} // 后面的覆盖前面的属性,产生新对象
Object方法
用Object.assign
可以实现相同的功能,不过代码比较繁琐,适合直接修改对象内容
const a = {a:1,b:2}
const b = {b:3}
const c = Object.assign({}, a, b, {a:3}) // 后面的覆盖前面的属性
对象操作
对象操作方法很多,这里使用一些常用方法
const a = {} // 新建对象,一般用这种字面量方式,不用new Object(),语法简洁
const b = 'name'
const c = {
a: a,
b,
[b]: '测试名字'
}
// 常用方法
const d = Object.assign({}, c)
Object.freeze(d) // 冻结对象,任何数据修改不能生效
Object.hasOwn(c, 'name') // 是否存在属性判断,代替c.hasOwnProperty('name')
Object.fromEntries([['a', '1'], ['b', '2']]) // 数组转对象
Object.entries(d) // 对象转数组
//====================================================
const arr = [{id:1, name: 'n1'}, {id:2, name: 'n2'}]
const obj1 = Object.fromEntries(arr.map(item=>[item.id, item])) // 常见的数据数组转成对象格式
const obj2 = arr.reduce((res, item) => {
res[item.id] = item
return res
}, {}) // 常见的数据数组转成对象格式
// ===================================================
Object.defineProperty(obj2, 'name', {}) // 定义变量描述符,重要,vue2响应式原理
数组操作
JS
中除了对象就是数组,数组操作很多,尤其是ES6
之后,增加了大量的方法
参考:https://www.runoob.com/jsref/jsref-obj-array.html
const arr = [1,2,5,3,4,1,3]
const arr1 = arr.concat([3,4,4]) // 数组合并,不改变源arr
const arr11 = [...arr, 3,4,4] // 数组合并,不改变源arr
const h = Math.max(...arr) // 展开传参
arr.push(1) // 后面增加
arr.pop() // 后面弹出一个,返回弹出的值
arr.unshift(1) // 前面增加
arr.shift() // 前面删除
arr.join(',') // 转成字符串,很有用
arr.slice(1, 3) // 数组按照序号截取[start,end)
arr.splice(1, 1) // 从指定的index删除n个数据
const arr2 = arr.map(item => item * 2) // 映射
const arr3 = arr.filter(item => item<4) // 过滤
const item1 = arr.find(item => item<4) // 查找第一个
const itemIndex = arr.findIndex(item=> item===5) // 查询序号
// ===========去重=========
const arr4 = [...new Set(arr)] // 最简单去重,不过如果是判断对象内部属性去重就不能使用了
const arr5 = arr.reduce((res, item) => {
!res.includes(item) && res.push(item)
return res
}, []) // 用reduce函数去重可以自定义规则
arr.fill(0) // arr全部赋值成指定的值
// 伪数组转换数组
const arr6 = Array.prototype.slice.call(arguments, 0)
const arr7 = Array.from(arguments)
const arr8 = [...arguments]
对象深拷贝
前面使用的展开运算符或者Object.assign
都是浅拷贝
如果要实现深层拷贝,需要自己实现或者用第三方库,比如lodash
的cloneDeep
,一般都是递归复制对象,简单项目不想引入第三方库,也可以用JSON
,不过需要注意JSON
不支持嵌套对象互相引用。
const a = {a:1,b:{c:2}}
function simpleClone(obj){
return JSON.parse(JSON.stringify(obj))
}
const d = simpleClone(a) // 深拷贝
console.log(a.b===d.b) // false
const e = {...a} // 浅拷贝
console.log(a.b===e.b) // true
解构赋值
ES6中有个很重要的改变,就是结构赋值,相当方便
// 对象结构
const a = {b:1, c:2, d:3}
const {b,c} = a // 从a中结构出b和c
const {d: d1} = a // 从a中结构出d并修改名为d1,主要是在结构后重名的情况下使用
// 数组结构
const f = [1, 2, 3, 4]
const [g, h] = f // 解构f数组的0和1位置数据给g和h
let i = 1
let j = 2
// [i, j] = [j, i] // 解构交换两个变量的值
function h(config){
console.log(config.i, config.j)
}
h({i, j}) // 解构传参
function h1({i, j}){ // 参数直接解构
console.log(i, j)
}
h1({i, j}) // 解构传参
注意:如果是Vue
响应式数据要解构要谨慎,解构后可能不响应
Vue
解构问题:https://cn.vuejs.org/guide/essentials/reactivity-fundamentals.html#limitations-of-reactive
网页定时
网页定时常用的就是setInterval()
和setTimeout()
const handler1 = () => {
console.log('setInterval')
}
const handler2 = () => {
console.log('setTimeout')
timer2=setTimeout(handler2, 1000)
}
const timer1 = setInterval(handler1, 1000)
let timer2 = setTimeout(handler2, 1000)
setTimeout(handler2) // 加入延时队列,异步执行
Promise.resolve().then(handler2) // 加入微队列,异步执行,Vue的nextTick
注意:定时器一般要在销毁的时候清理掉,尤其是单页应用要注意清理定时器
Promise
Promise
是新的一步函数处理模式,ES6
之前也有Promise A+
规范,有不少第三方实现,比如jQuery
也有自己的实现。
Promise实例
Promise
是一个对象,有一个then
方法,基本就可以认为是一个Promise
对象了,支持async和await
了
// 新建Promise,then或catch等都返回一个新的Promise对象
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
})
.then((data) => {
console.log("then1", data);
return 2;
})
.then((data) => {
console.log("then2", data);
return Promise.resolve(3);
})
.then((data) => {
console.log("then3", data);
})
.then((data) => {
console.log("then4", data);
});
// promise.then方法有两个参数一个表示成功,一个表示失败
promise.then(successFunc, failFunc)
// 等价于
promise.then(successFun) // 成功
.catch(failFunc) // 失败
.finally((err) => console.log("finally", err)); // 无论成功或失败都调用
Promise静态方法
常用一些静态方法,用于操作或生成Promise
对象实例
// 常用方法
Promise.resolve(1) // 生成直接完成Promise
Promise.reject(1) // 生成直接拒绝Promise
Promise.all([]) // 等待多个Promise,都成功才成功,失败一个就reject
Promise.allSettled([]) // 等待多个Promise,执行完,失败也包含在结果内
Promise.any([]) // 有一个成功就返回,都失败时拒绝
async和await
async
和await
是为了简化Promise
开发,其实是语法糖,有一些注意事项
-
await
后面的代码类似于在then
函数中执行,必须给函数添加async
-
async function a(){ console.log('开始执行执行1') const promise = new Promise(resolve=>{ // 生成Promise console.log('生成Promise2') setTimeout(()=>{ resolve() }, 1000) }) promise.then(()=>{ console.log('then:等待1秒后执行3') }) await promise console.log('await:等待1秒后执行4') } a()
-
-
加了
async
的函数无论原来返回什么,都是返回Promise
对象了-
function a(){ return 1 } async function b(){ // 打上async标记后,返回值就不再是1了 return 1 } function c(){ return Promise.resolve(1) } async function d(){ return Promise.resolve(1) // async函数如果已经返回Promise,就用返回的Promise } console.log(a()) console.log(b()) console.log(c()) // b和c函数基本等价 console.log(d())
-
-
不是所有函数都支持被变成异步函数,有些直接依赖返回值的函数不能用async
- 作为回调函数需要注意一下是否支持返回
Promise
- 作为回调函数需要注意一下是否支持返回
-
await
异常处理,await
遇到reject
的话,将抛出异常-
async function a(){ await Promise.reject(new Error('Test Error')) console.log('执行不到这里') } a() // 异常处理方式1 async function b(){ await Promise.reject(new Error('Test Error')) .catch(err=>console.log(err)) // catch之后正常执行,catch之后返回一个新的Promise,状态是fulfilled console.log('可以执行到这里') } b() // 异常处理方式2 async function c(){ try{ await Promise.reject(new Error('Test Error')) }catch(err){ console.log(err) } console.log('可以执行到这里') } c()
-
-
多个异步函数等待问题,如果业务支持并发等待,不要使用顺序
await
等待-
async function a(input){ await new Promise((resolve)=>{ setTimeout(function(){ console.log(
function a ${input}
) resolve() }, 3000) }) } async function b(input){ await new Promise((resolve)=>{ setTimeout(function(){ console.log(function b ${input}
) resolve() }, 3000) }) } async function c(){ await a('c') await b('c') // 顺序等待,是否是业务需要这样? console.log('c执行完成') } c() async function d(){ const pa = a('d') const pb = b('d') // 并发等待 // await Promise.all([pa, pb]) await pa await pb console.log('d执行完成') } d()
-
模块化
ES6
支持模块化代码,ES6
之前,js
代码在引入之后直接执行,产生的变量可能影响全局对象。
平时使用打包工具可能还会接触到nodejs
的模块化
ES6模块
导出模块,假设文件为:./utils/index.js
:
export const a = '1' // 命名导出
const b = '2'
export { b as nb } // 导出重命名
export default { // 默认导出只能有一个
c: '3'
}
上面导出对象结构类似:
{
a: '1',
nb: '2',
default: {
c: '3'
}
}
导入模块:
import dayjs from 'dayjs' // 从库中导入default模块
import { createApp } from 'vue' // 命名导入,有点类似解构
import { a as na } from './utils' // 导入重命名
import * as allUtils from './utils' // 导入全部 allUtils.default为默认导出数据
const comp = import('./utils') // import函数异步导入,返回Promise
注意:尽量使用命名导入和导出,现代打包工具一般都支持treeshaking
,没有用到的导出可以在打包的时候不打入构建结果中,减少js体积
Nodejs模块
NodeJS
中使用CommonJS
规范模块化,使用require
导入,一般了解即可
导出模块:
const a = 1
module.exports = {
a
}
module.exports.b = 2
导入模块:
const fs = require('fs') // 导入库
const utils = require('./utils') // 本地导入