变量类型
1. typeof能判断哪些类型
- 识别所有值类型
- 识别函数,识别为function
- 判断是否是引用类型(不可再细分),识别为Object
2. 何时使用===,何时使用==
-
==运算符
除了 == null 之外,其他一律用 === ,例如:
const obj = {x: 100}
if(obj.a == null) {}
//相当于if(obj.a === null || obj.a === undefined ) {}
- ===运算符
3. 值类型和引用类型的区别
-
值类型在栈中存储(key-value)。
字符串(string)、数值(number)、布尔值(boolean)、undefined、null (这5种基本数据类型是按值访问的,因为可以操作保存在变量中的实际的值),ECMAScript 2016新增了一种基本数据类型:symbol
-
引用类型在栈中存储(key-value,value为内存地址,指向堆),在指向的堆地址中存储实际值,基于内存和CPU运行时间的考虑,引用类型传值时传地址。
对象(Object)、数组(Array)、函数(Function,不用于存储数据,所以没有拷贝、复制函数一说)、Date、RegExp、null(特殊引用类型,指针指向为空地址)
当a = 10, let b = a; b在栈中拥有新的内存,拷贝a的value值20;
当a = {age: 20}, let b = a; b在栈中拥有新的内存,拷贝a的value值,即a,b 指向堆中同一地址——{age:20};
4. 手写深拷贝
- 注意判断值类型和引用类型
- 注意判断是数组还是对象
- 递归
function deepClone(obj = {}) {
if (typeof obj !== 'object' || obj == null) {
// obj 是 null ,或者不是对象和数组,直接返回
return obj
}
// 初始化返回结果
let result
if (obj instanceof Array) {
result = []
} else {
result = {}
}
for (let key in obj) {
// 保证 key 不是原型的属性
if (obj.hasOwnProperty(key)) {
// 递归调用!!!
result[key] = deepClone(obj[key])
}
}
// 返回结果
return result
}
变量计算
1. 字符串拼接
eg:
const a = 10 + 10; //20
const b = 10 + '10'; //'1010'
const c = true +'10' //'true10'
字符串转int
const d = 100 + parseInt('10'); //110
2. ==
除了 == null 之外,其他一律用 === ,例如:
const obj = {x: 100}
if(obj.a == null) {}
//相当于if(obj.a === null || obj.a === undefined ) {}
3. if语句和逻辑运算
1)if语句
if()判断规则:判断的是truly变量和falsely变量,而非true或false;
-
truly变量: !!a === true;
-
falsely变量: !!b === false;
除以下变量,都是truely变量 0, NaN, '', null, undefined, false
2)逻辑运算
逻辑判断规则:&& 遇到falsely变量即返回, || 遇到truly变量即返回; console.log( 10 && 0 ) //0
console.log( 10 && 0 ) // 0
console.log( 10 || 0 ) // 10
原型和原型链
以ES6的class来讲:
-
如何准确判断一个变量是不是数组
typeof 或 查看其原型链; -
手写一个简易的jQuery,考虑插件和扩展性
class jQuery {
constructor(selector) {
const result = document.querySelectorAll(selector)
const length = result.length
for (let i = 0; i < length; i++) {
this[i] = result[i]
}
this.length = length
this.selector = selector
}
get(index) {
return this[index]
}
each(fn) {
for (let i = 0; i < this.length; i++) {
const elem = this[i]
fn(elem)
}
}
on(type, fn) {
return this.each(elem => {
elem.addEventListener(type, fn, false)
})
}
// 扩展很多 DOM API
}
// 插件
jQuery.prototype.dialog = function (info) {
alert(info)
}
// “造轮子”
class myJQuery extends jQuery {
constructor(selector) {
super(selector)
}
// 扩展自己的方法
addClass(className) {
}
style(data) {
}
}
// const $p = new jQuery('p')
// $p.get(1)
// $p.each((elem) => console.log(elem.nodeName))
// $p.on('click', () => alert('clicked'))
- class的原型本质,怎么理解
1. class
- constructor
- 属性
- 方法
// 类
class Student {
constructor(name, number) {
this.name = name
this.number = number
// this.gender = 'male'
}
sayHi() {
console.log(
`姓名 ${this.name} ,学号 ${this.number}`
)
// console.log(
// '姓名 ' + this.name + ' ,学号 ' + this.number
// )
}
// study() {
// }
}
class Student {}
// 通过类 new 对象/实例
const xialuo = new Student('夏洛', 100)
console.log(xialuo.name)
console.log(xialuo.number)
xialuo.sayHi()
const madongmei = new Student('马冬梅', 101)
console.log(madongmei.name)
console.log(madongmei.number)
madongmei.sayHi()
2. 继承
- extends
- super
- 扩展或重写
// 父类
class People {
constructor(name) {
this.name = name
}
eat() {
console.log(`${this.name} eat something`)
}
}
// 子类
class Student extends People {
constructor(name, number) {
super(name)
this.number = number
}
sayHi() {
console.log(`姓名 ${this.name} 学号 ${this.number}`)
}
}
// 子类
class Teacher extends People {
constructor(name, major) {
super(name)
this.major = major
}
teach() {
console.log(`${this.name} 教授 ${this.major}`)
}
}
// 实例
const xialuo = new Student('夏洛', 100)
console.log(xialuo.name)
console.log(xialuo.number)
xialuo.sayHi()
xialuo.eat()
// 实例
const wanglaoshi = new Teacher('王老师', '语文')
console.log(wanglaoshi.name)
console.log(wanglaoshi.major)
wanglaoshi.teach()
wanglaoshi.eat()
3. 类型判断 - instanceOf
- instanceOf:判断是否属于当前类或当前类的父类;
- Object是所有class的父类
4. 原型
获取Student的实例xialuo的属性或执行其方法时,先在自身属性和方法中查找,若找不到,则去其__proto__隐式原型中查找
引自该文 不错的: juejin.cn/post/693449… 最后一个
null,设计上是为了避免死循环而设置的,Object.prototype的隐式原型指向null。
//class实际上是函数
typeof People // 'function'
//显式原型和隐式原型
__proto__ 隐式原型;
prototype 显式原型
5. 作用域和闭包
- this在不同应用场景下如何取值(见下述 2)this)
- 手写bind函数
- 实际开发中闭包怎么用,举例说明
1)作用域
从当前作用域向上寻找最近定义的变量
-
全局作用域
-
函数作用域
-
块级作用域(ES6新增)
如if、while后的大括号中
/* !重要:绑定的click函数在执行时才会触发
若let i = 0放在for循环外定义,i的作用域是全局,那么当循环很快执行完,而i值此时已经为10,之后每次点击
触发click时i都是10,
但是当 let i = 0放在循环内定义,for循环每次执行都会形成一个新的块,每当执行click都会从当前块寻找变量
i的值,此时得到的是不同的i值 */
let a
for (let i = 0; i < 10; i++) {
a = document.createElement('a')
a.innerHTML = i + '<br>'
a.addEventListener('click', function (e) {
e.preventDefault()
alert(i)
})
document.body.appendChild(a)
}
console.log('a')
2)this
this在不同场景中取什么样的值是在函数执行时确认,不是在函数定义时确认
1.
function fn1() {
console.log(this)
}
fn1() //window
2.
fn1.call({x: 100}) // {x: 100}
3.
// bind 返回一个新的函数执行
const fn2 = fn1.bind({x: 200})
fn2() // {x: 200}
4.
const zhangsan = {
name:'张三',
sayHi(){
//this 即当前对象
console.log(this)
},
wait(){
setTimeout(function() {
//this === window,和1的情况fn一样,是一个函数被执行,是setTimeout触发的执行,而不是
//zhangsan触发的
console.log(this)
})
}
}
5. (和4对比
const zhangsan = {
name:'张三',
sayHi(){
//this 即当前对象
console.log(this)
},
wait(){
setTimeout(() => {
//this 即当前对象,箭头函数的this永远取上级作用域的this
console.log(this)
})
}
}
- 作为普通函数
- 使用call apply bind
- 作为对象方法被调用
- 在class方法中被调用
- 箭头函数(箭头函数的this永远取上级作用域的this,见上面代码的例5)
3)实际开发中闭包应用
- 隐藏数据
- 做一个简单的cache工具(当在function外,令data.b = 200, 由于data在当前作用域并未定义,所以无法修改其内数据)
// 闭包隐藏数据,只提供 API
function createCache() {
const data = {} // 闭包中的数据,被隐藏,不被外界访问
return {
set: function (key, val) {
data[key] = val
},
get: function (key) {
return data[key]
}
}
}
//重要:data.b = 200, 由于data在当前作用域并未定义,所以无法修改其内数据
const c = createCache()
c.set('a', 100)
console.log( c.get('a') )
异步
题目
- 同步和异步的区别
- 手写Promise加载一张图片
//加载图片
function loadImg(src) {
const p = new Promise(
(resolve,reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error(`图片加载失败 ${src}`)
reject(err)
}
}
)
return p;
}
// 真/假地址
const url1 = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
const url2 = 'https://img3.mukewang.com/5a9fc8070001a82402060220-100-100.jpg'
//真地址触发resolve,假地址触发reject
loadImg(url1).then(img1 => {
console.log(img1.width)
return img1 // 普通对象
}).then(img1 => {
console.log(img1.height)
return loadImg(url2) // promise 实例
}).then(img2 => {
console.log(img2.width)
return img2
}).then(img2 => {
console.log(img2.height)
}).catch(ex => console.error(ex))
- 前端使用异步的场景(看是否真有开发经验)
异步和同步
- 基于JS是单线程语言
- 异步不会阻塞代码执行
- 同步会阻塞代码执行
单线程和异步
- JS是单线程语言,只能同时做一件事
- 浏览器和nodejs已支持JS启动进程,如Web Server
- JS和DOM渲染共用一个线程,因为JS可以修改DOM结构
- 遇到等待(网络请求,定时任务)不能卡住
- 需要异步
- 回调callback函数形式
应用场景
-
网络请求,如ajax图片加载
-
定时任务,如setTimeout
callback hell 和 Promise
Promise将callback变成非嵌套的形式,而是管道串联的形式
异步进阶
题目
- 描述 event loop 运行机制(可画图)
- Promise 哪几种状态,如何变化?
- 宏任务和微任务的区别
- 场景题:Promise catch 连接 then
- 场景题:Promise 和 setTimeout 顺序
- 场景题:各类异步执行顺序问题
1. Promise catch 连接 then
// 第一题
Promise.resolve().then(() => {
console.log(1)
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 1 3
// 第二题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 1 2 3
// 第三题
Promise.resolve().then(() => {
console.log(1)
throw new Error('erro1')
}).catch(() => {
console.log(2)
}).catch(() => { // 注意这里是 catch
console.log(3)
})
// 1 2
2. async/await 语法问题
async function fn() {
return 100
}
(async function () {
const a = fn() // ?? // promise
const b = await fn() // ?? // 100
})()
(async function () {
console.log('start')
const a = await 100
console.log('a', a)
const b = await Promise.resolve(200)
console.log('b', b)
const c = await Promise.reject(300)
console.log('c', c)
console.log('end')
})() // 执行完毕,打印出那些内容?
3. Promise 和 setTimeout 顺序
console.log(100)
setTimeout(() => {
console.log(200)
})
Promise.resolve().then(() => {
console.log(300)
})
console.log(400)
// 100 400 300 200
4. 执行顺序问题(经典)
网上很经典的面试题
async function async1 () {
console.log('async1 start') // 2
// 这一句会同步执行,返回 Promise ,其中的 `console.log('async2')` 也会同步执行
await async2()
//上面有 await ,下面就变成了“异步”,类似 cakkback 的功能(微任务)
console.log('async1 end') // 6
}
async function async2 () {
console.log('async2') // 3
}
console.log('script start') // 1
setTimeout(function () { // 异步,宏任务
console.log('setTimeout') // 8
}, 0)
async1()
new Promise (function (resolve) { // 返回 Promise 之后,即同步执行完成,then 是异步代码
console.log('promise1') // 4 Promise 的函数体会立刻执行
resolve()
}).then (function () { // 异步,微任务
console.log('promise2') // 7
})
console.log('script end') // 5
// 同步代码执行完之后,屡一下现有的异步未执行的,按照顺序
// 1. async1 函数中 await 后面的内容 —— 微任务
// 2. setTimeout —— 宏任务
// 3. then —— 微任务
event loop
-
JS单线程运行
-
异步(setTimeout ajax)基于回调来实现 -
event loop就是异步回调的实现原理 -
DOM事件(如点击)也使用回调,基于event loop -
setTimeout ajax 点击等都会触发回调,只是时机不同
1. JS如何执行
- 从前向后一行一行执行
- 若某一行报错,则停止下面代码的执行
- 先把同步代码执行完,再执行异步
2. event loop过程
- 同步代码,一行一行放在
Call Stack执行 - 遇到异步,会先记录下(在
Web APIS中处理异步,如放置定时器),等待时机(定时、网络请求等) - 时机到了,就移动到
Callback Queue - 若
Callback Queue为空,Event Loop开始工作 - 轮询查找
Callback Queue,如有则移动到Call Stack执行 - 然后继续轮询查找(like永动机)
eg:
console.log('Hi')
setTimeout(function cb1() {
console.log('cb1') // cb 即 callback
}, 5000)
console.log('Bye')
3. DOM 事件,也基于event loop实现
<button id="btn1">提交</button>
<script>
console.log('Hi')
$('#btn1').click(function (e) {
console.log('button clicked')
})
console.log('Bye')
</script>
今日小tips:当一行代码后没有分号,而下一行是一个以()包裹的函数,在()前加一个!即可。
Promise
- 三种状态
- 状态和 then catch
- 常用 API
先回顾一下 Promise 的基本使用
// 加载图片
function loadImg(src) {
const p = new Promise(
(resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error(`图片加载失败 ${src}`)
reject(err)
}
img.src = src
}
)
return p
}
const url = 'https://img.mukewang.com/5a9fc8070001a82402060220-140-140.jpg'
loadImg(url).then(img => {
console.log(img.width)
return img
}).then(img => {
console.log(img.height)
}).catch(ex => console.error(ex))
1. 三种状态
- pending resolved rejected
(画图表示转换关系,以及转换不可逆)
// 刚定义时,状态默认为 pending
const p1 = new Promise((resolve, reject) => {
})
// 执行 resolve() 后,状态变成 resolved
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
})
})
// 执行 reject() 后,状态变成 rejected
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject()
})
})
// 直接返回一个 resolved 状态
Promise.resolve(100)
// 直接返回一个 rejected 状态
Promise.reject('some error')
2. 状态、状态变化和 then catch
状态变化会触发 then catch —— 这些比较好理解,就不再代码演示了
- pending 不会触发任何 then catch 回调
- 状态变为 resolved 会触发后续的 then 回调
- 状态变为 rejected 会触发后续的 catch 回调
const p1 = Promise.resolve(100) // resolved
p1.then(data => { // 执行
console.log(data)
}).catch(err => {
console.log('data1',data)
})
const p2 = Promise.reject('err')// rejected
p2.then(data => { // 不执行
console.log(data)
}).catch(err => {
console.log('err2',err)
})
then catch 会继续返回 Promise ,此时可能会发生状态变化!!!
- then正常返回resolved,里面有报错则返回rejected
- catch正常返回resolved,里面有报错则返回rejected
// then() 一般正常返回 resolved 状态的 promise
Promise.resolve().then(() => {
return 100
})
// then() 里抛出错误,会返回 rejected 状态的 promise
Promise.resolve().then(() => {
throw new Error('err')
})
// catch() 不抛出错误,会返回 resolved 状态的 promise
Promise.reject().catch(() => {
console.error('catch some error')
})
// catch() 抛出错误,会返回 rejected 状态的 promise
Promise.reject().catch(() => {
console.error('catch some error')
throw new Error('err')
})
看一个综合的例子,即那几个面试题
// 第一题
Promise.resolve().then(() => {
console.log(1)
}).catch(() => {
console.log(2)
}).then(() => {
console.log(3)
})
// 第二题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1)
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2)
}).then(() => {
console.log(3)
})
// 第三题
Promise.resolve().then(() => { // 返回 rejected 状态的 promise
console.log(1)
throw new Error('erro1')
}).catch(() => { // 返回 resolved 状态的 promise
console.log(2)
}).catch(() => {
console.log(3)
})
async/await
- 语法介绍
- 和 Promise 的关系
- 异步本质
- for...of
有很多 async 的面试题,例如
- async 直接返回,是什么
- async 直接返回 promise
- await 后面不加 promise
- 等等,需要找出一个规律
1. 语法介绍
用同步的方式,编写异步,消灭回调函数。 执行await必须用async函数作为包裹
function loadImg(src) {
const promise = new Promise((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
reject(new Error(`图片加载失败 ${src}`))
}
img.src = src
})
return promise
}
async function loadImg1() {
const src1 = 'http://www.imooc.com/static/img/index/logo_new.png'
const img1 = await loadImg(src1)
return img1
}
async function loadImg2() {
const src2 = 'https://avatars3.githubusercontent.com/u/9583120'
const img2 = await loadImg(src2)
return img2
}
async function loadimg(){
const src = 'ok';
const img2 = await loadImg(src2);
return img2;
}
(async function () {
// 注意:await 必须放在 async 函数中,否则会报错
try {
// 加载第一张图片
const img1 = await loadImg1()
console.log(img1)
// 加载第二张图片
const img2 = await loadImg2()
console.log(img2)
} catch (ex) {
console.error(ex)
}
})()
2. 和 Promise 的关系
async/await是消灭异步回调的武器,但和Promise并不互斥,反而相辅相成:
- async 函数返回结果都是 Promise 对象(如果函数内没返回 Promise ,则自动封装一下)
- await相当于Promise的then
- try-catch可以捕获异常,代替了Promise的catch
async function fn1() {
return 100
}
const res = fn1()
res.then(data => {
console.log('data', data)
}) //100
! (async function () {
const p1 = Promise.resolve(200)
const data = await p1 // await 相当于Promise的then
console.log('data',data)
})() // 200
! (async function () {
const p1 = Promise.resolve(200)
const data = await 400 // await Promise.resolve(400)
console.log('data',data)
})() // 400
! (async function () {
const data2 = await fn1()
})()
! (async function (){
const p4 = Promise.reject('err')
try{
const res = await p4
console.log(res)
}catch{
console.error(ex) // try...catch 相当于 promise 的 catch
}
})
- await 后面跟 Promise 对象:会阻断后续代码,等待状态变为 resolved ,才获取结果并继续执行
- await 后续跟非 Promise 对象:会直接返回
(async function () {
const p1 = new Promise(() => {})
await p1
console.log('p1') // 不会执行
})()
(async function () {
const p2 = Promise.resolve(100)
const res = await p2
console.log(res) // 100
})()
(async function () {
const res = await 100
console.log(res) // 100
})()
(async function () {
const p3 = Promise.reject('some err')
const res = await p3
console.log(res) // 不会执行
})()
- try...catch 捕获 rejected 状态
(async function () {
const p4 = Promise.reject('some err')
try {
const res = await p4
console.log(res)
} catch (ex) {
console.error(ex)
}
})()
总结来看:
- async 封装 Promise
- await 处理 Promise 成功
- try...catch 处理 Promise 失败
3. 异步本质
-
async/await 消灭异步回调
-
JS还是单线程,还得是有异步,还得是基于event loop
-
async/await 只是一个语法糖 不过很香
-
await 是同步写法,但本质还是异步调用。
await:顾名思义, await 就是异步等待,它等待的是一个Promise,因此 await 后面应该写一个Promise对象,如果不是Promise对象,那么会被转成一个立即 resolve 的Promise。 async 函数被调用后就立即执行,但是一旦遇到 await 就会先返回,等到异步操作执行完成,再接着执行函数体内后面的语句。总结一下就是:async函数调用不会造成代码的阻塞,但是await会引起async函数内部代码的阻塞。如下例:
async function async1 () {
console.log('async1 start') // 2
await async2()
// await后的代码都属于异步回调的内容,
// 类似event loop、setTimeout(cb1)
// setTimeout( () => {console.log('async1 end')})
console.log('async1 end') // 5 关键在这一步,它相当于放在 callback 中,最后执行
}
async function async2 () {
console.log('async2') // 3
}
console.log('script start') // 1
async1()
console.log('script end') // 4
// 同步代码执行完 event loop开始轮询
即,只要遇到了 await ,后面的代码都相当于放在 callback 里。
4. for...of
- for...in(and forEach for) 是常规的同步遍历
- for...of属于异步遍历
// 定时算乘法
function multi(num) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(num * num)
}, 1000)
})
}
// 使用 forEach ,是3 个值是一起被计算出来,1s 之后打印出所有结果,即
function test1 () {
const nums = [1, 2, 3];
nums.forEach(async x => {
const res = await multi(x);
console.log(res);
})
}
test1();
// 使用 for...of ,可以让计算挨个串行执行
async function test2 () {
const nums = [1, 2, 3];
for (let x of nums) {
// 在 for...of 循环体的内部,遇到 await 会挨个串行计算
const res = await multi(x)
console.log(res)
}
}
test2()
Web APIS
- JS基础知识 —— ECMA规定
- JS Web APIS —— W3C规定
- DOM BOM 时间绑定 Ajax 存储
DOM
题目
-
DOM 是哪种数据结构
-
DOM 操作的常用API
-
attr和property的区别
建议:非必须的话尽量使用property操作,因为property在js机制中可以避免一些重复渲染(耗费系统性能)
-
property
修改对象属性,不会体现在HTML结构中 -
attribute
修改html属性,会改变HTML结构 -
二者都有可能引起DOM重新渲染
// property 修改对象属性,不会体现在HTML结构中 // 通过修改DOM元素的js变量的形式来修改页面的渲染 const pList = document.querySelectorAll('p') const p = pList[0] p.style.width = '100px' console.log(p.style.width) p.className = 'red' console.log( p.nodeName ) // p console.log( p.nodeType ) // 1 // attribute 修改html属性,会改变HTML结构 // 直接修改标签的内容 p.setAttribute('data-name','imooc') console.log(p.getAttribute('data-name')) p.setAttribute('style','font-size: 50px')
-
-
一次性插入多个DOM节点,考虑性能
知识点
-
DOM 本质 —— 树
-
DOM 节点操作
-
获取节点:通过id,TagName,ClassName; All
const div1 = document.getElementById('div1') console.log('div1', div1) const divList = document.getElementsByTagName('div') // 集合 console.log('divList.length', divList.length) console.log('divList[1]', divList[1]) const containerList = document.getElementsByClassName('container') // 集合 console.log('containerList.length', containerList.length) console.log('containerList[1]', containerList[1]) const pList = document.querySelectorAll('p') console.log('pList', pList) const pList = document.querySelectorAll('p') const p1 = pList[0]
-
-
DOM 结构操作
-
修改property、attribute
-
新建、插入、移动节点(将现有元素插入append到其他节点,就会移动该元素至其他节点)
const div1 = document.getElementById('div1') //new const newP = document.createElement('p') newP.innerHTML = 'this is newP' //append div1.appendChild(newP) // move const p1 = document.getElementById('p1') div2.appendChild(p1) // 获取父元素 console.log(p1.parentNode) // 获取子元素列表 const div1ChildNodes = div1.childNodes; console.log(div1ChildNodes) // 本应打印3个,却打印了7个 const div1ChildNodesP = Array.prototype.slice.call(div1.childNodes).filter(child => { if( child.nodeType === 1) return true return false })
-
-
DOM 性能 (重要)
-
DOM操作非常昂贵,避免频繁的DOM操作
-
对DOM 查询做缓存
// 不缓存DOM 查询结果 for(let i = 0; i<document.getElementsByTagName('p').length; i++){ //每次循环都会计算 length ,频繁进行DOM 查询 } // 缓存DOM 查询结果 const pList = document.getElementsByTagName('p') const length = pList.length for(let i = 0; i<length; i++){ //缓存length 只进行一次 DOM 查询 } -
将频繁操作改为一次性操作
const list = document.getElementById('list') // 创建一个文档片段,此时还没有插入到 DOM 结构中 const frag = document.createDocumentFragment() for (let i = 0; i < 20; i++) { const li = document.createElement('li') li.innerHTML = `List item ${i}` // 先插入文档片段中 frag.appendChild(li) } // 都完成之后,再统一插入到 DOM 结构中 list.appendChild(frag) console.log(list)
-