前端面试复习之JS

249 阅读8分钟

变量类型

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. 继承

  1. extends
  2. super
  3. 扩展或重写
// 父类
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

  1. instanceOf:判断是否属于当前类或当前类的父类;
  2. Object是所有class的父类

4. 原型

获取Student的实例xialuo的属性或执行其方法时,先在自身属性和方法中查找,若找不到,则去其__proto__隐式原型中查找

引自该文 不错的: juejin.cn/post/693449… 最后一个 null,设计上是为了避免死循环而设置的, Object.prototype 的隐式原型指向 null

image.png

//class实际上是函数
typeof People // 'function' 

//显式原型和隐式原型
__proto__ 隐式原型;
prototype 显式原型

5. 作用域和闭包

  • this在不同应用场景下如何取值(见下述 2)this)
  • 手写bind函数
  • 实际开发中闭包怎么用,举例说明

1)作用域

从当前作用域向上寻找最近定义的变量

  • 全局作用域

  • 函数作用域

  • 块级作用域(ES6新增)

    ifwhile后的大括号中
       
    
/* !重要:绑定的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>

IMG_0679(20220225-164745).JPG

今日小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)