优秀的编程知识分享平台

网站首页 > 技术文章 正文

Javascript初级知识点(原型,闭包,异步)整理

nanyue 2024-10-24 11:44:29 技术文章 2 ℃

JS

变量类型和计算

  • typeof 能判断出哪些类型
  • 何时使用 === 何时使用 ==
  • 值类型和引用类型的区别
  • 手写深拷贝

值类型和引用类型

值类型存储在栈内存中,引用类型存储在堆内存中

// 值类型
let a = 100
let b = a
a = 200
console.log(b)  // 100
// 引用类型
let a = { age: 20 }
let b = a
b.age = 21
console.log(a.age)  // 21

常见值类型和引用类型

// 常见值类型
let u   // undefined
const s = 'abc'
const n = 100
const b = true
const s = Symbol('s')
// 常见引用类型
const obj = { x: 100 }
const arr = ['a', 'b', 'c']

const n = null // 特殊引用类型,指针指向为空地址

// 特殊的引用类型,但不用于存储数据,所以没有拷贝、复制函数这一说
function fun() {}

类型判断

typeof 运算符

  • 识别所有值类型
  • 识别函数
  • 判断是否是引用类型(不可再细分)

深克隆

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
}

类型转换

  • 字符串拼接
  • ==
  • if 语句逻辑运算
const a = 100 + 10 // 110
const b = 100 + '10' // 10010
const c = true + '10' // true10

const d = 100 + parseInt('10') // 110

==

100 == '100' // true
0 == '0' // true
0 == false // true
false == '' // true
null == undefined // true
NaN == NaN // false

// 除了 == null 之外,其他都一律用 === ,例如:
const obj = { x: 100 }
if (obj.a == null) {}
// 相当于
if (obj.a === null || obj.a === undefined) {}

逻辑运算

if 语句和逻辑运算

  • truly 变量:!!a === true 的变量
  • falsely 变量:!!a === false 的变量

原型和原型链

  • 如何判断一个变量是不是数组?
  • 手写一个简易的 jQuery,考虑插件和扩展性
  • class 的原型本质,怎么理解?

class

class Student {
    constructor(name, number) {
        this.name = name
        this.number = number
    }
    greeting() {
        console.log(`Hello, My name is ${this.name}, number is ${this.number}`)
    }
}

// 实例化
const zhangsan = new Student('zhangsan', 1)
zhangsan.greeting() // Hello, My name is zhangsan, number is 1

继承

// 父类
class Person {
    constructor(name) {
        this.name = name
    }
    eat() {
        console.log(`${this.name} eat something`)
    }
}

// 子类
class Student extends Person {
    constructor(name, number) {
        super(name)
        this.number = number
    }
    greeting() {
        console.log(`Hello, My name is ${this.name}, number is ${this.number}`)
    }
}

// 子类
class Teacher extends Person {
    constructor(name, subject) {
        super(name)
        this.subject = subject
    }
    teach() {
        console.log(`My name is ${this.name}, and I am teaching ${this.subject}`)
    }
}

// 实例化
const zhangsan = new Student('zhangsan', 1)
zhangsan.greeting() // Hello, My name is zhangsan, number is 1
zhangsan.eat() // zhangsan eat something

const mrWang = new Teacher('wang', 'Math')
mrWang.eat()  // wang eat something
mrWang.teach()  // My name is wang, and I am teaching Math

// 类型判断
console.log(zhangsan instanceof Student) // true
console.log(zhangsan instanceof Person) // true
console.log(zhangsan instanceof Object) // true

// true
console.log([] instanceof Array)
console.log([] instanceof Object)
console.log({} instanceof Object)

原型

// class 实际上是函数,语法糖而已
typeof Person // function
typeof Student // function

// 隐式原型和显示原型
console.log(zhangsan.__proto__)
console.log(Student.prototype)
console.log(zhangsan.__proto__ === Student.prototype) // true

原型关系

  • 每个 class 都有显示原型 prototype
  • 每个实例都有隐式原型 __ proto__
  • 实例的 __ proto__指向对应的 class 的 prototype

原型链

console.log(Student.prototype.__proto__)
console.log(Person.prototype)
console.log(Person.prototype === Student.prototype.__proto__) // true

instanceof

手动实现 instanceof

function myInstanceof(left, right) {
    // 获取类的原型
    let prototype = right.prototype
    // 获取对象的原型
    left = left.__proto__
    // 判断对象的类型是否等于类型的原型
    while (true) {
        if (left === null)
            return false
        if (prototype === left)
            return true
        left = left.__proto__
    }
}

手写一个简易的 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)
        })
    }
}

// 插件
jQuery.prototype.dialog = function (info) {
    alert(info)
}

// “造轮子”
class myJQuery extends jQuery {
    constructor(selector) {
        super(selector)
    }
    // 扩展自己的方法
    addClass(className) {
        // ...
    }
    style(data) {
        // ...
    }
}

作用域和闭包

  • this 的不同应用场景,如何取值?
  • 手写 bind 函数
  • 实际开发中闭包的应用场景,举例说明
  • 创建 10 个 <a> 标签点击的时候弹出对应的序号

作用域

let a = 0
function fn1() {
    let a1 = 100

    function fn2() {
        let a2 = 200

        function fn3() {
            let a3 = 300
            return a + a1 + a2 + a3
        }
        fn3()
    }
    fn2()
}
fn1()
  • 全局作用域
  • 函数作用域
  • 块级作用域(ES6新增)

自由变量

  • 一个变量在当前作用域没有定义,但被使用了
  • 向上级作用域,一层一层依次寻找,直到找到了为止
  • 如果在全局作用域都没有找到,则报错 xxx is not defined

闭包

  • 作用域应用的特殊情况,有两种表现
  • 函数作为参数被传递
  • 函数作为值返回
// 函数作为返回值
function create() {
    let a = 100
    return function () {
        console.log(a)
    }
}

let fn = create()
let a = 200
fn() // 100

// 函数作为参数
function print(fn) {
    let a = 200
    fn()
}

let a = 100
function fn() {
    console.log(a)
}
print(fn) // 100

闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找

不是在执行的地方!!!

this

  • 作为普通函数
  • 使用 call apply bind
  • 作为对象的方法被调用
  • 在 class 方法中调用
  • 箭头函数

this 取什么值是在函数执行的时候确定的,不是在定义的时候

function fn1() {
    console.log(this)
}
fn1() // window

fn1.call({x: 100}) // {x: 100}

const fn2 = fn1.bind({x: 200})
fn2() // {x: 200}

箭头函数

const zhangsan = {
    name: 'zhangsan',
    greeting() {
        // this 即当前对象
        console.log(this)
    },
    wait() {
        setTimeout(function() {
            // this === window
            console.log(this)
        })
    }
}

// 箭头函数的 this 永远取上级作用域的 this
const zhangsan = {
    name: 'zhangsan',
    // this 即当前对象
    greeting() {
        console.log(this)
    },
    wait() {
        setTimeout(() => {
            // this 即当前对象
            console.log(this)
        })
    }
}

创建 10 个 <a> 标签点击的时候弹出对应的序号

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)
}

手写 bind 函数

Function.prototype.myBind = function() {
    // 将参数拆解为数组
    const args = Array.prototype.slice.call(arguments)

    // 获取 this(数组第一项)
    const that = args.shift()

    // fn.bind(...) 中的 fn
    const self = this

    // 返回一个函数
    return function() {
        return self.apply(that, args)
    }
}

实际开发中闭包的应用

  • 隐藏数据
  • 如做一个简单的 cache 工具
  • function createCache() { const data = {} // 闭包中的数据,被隐藏,不被外界访问 return { set(key, value) { data[key] = value }, get(key) { return data[key] } } } const c = createCache() c.set('a', 100) console.log(c.get('a'))

异步

  • 同步和异步得区别是什么?
  • 手写 Promise 加载一张图片
  • 前端使用异步的场景
  • 请描述 event loop (事件循环/事件轮询)的机制,可画图
  • 什么是宏认为和微任务,两者有什么区别?
  • Promise 有哪三种状态?如何变化?
  • Promise 的 then 和 catch 的连接问题
  • async/await 语法
  • Promise 和 setTimeout 的顺序问题

单线程

  • JS 是单线程语言,只能同时做一件事
  • 浏览器和 nodejs 已支持 JS 启动进程,如 Web Worker
  • JS 和 DOM 渲染共用同一个线程,因为 JS 可修改 DOM 结构
  • 异步基于 回调 callback 函数形式

callback

// 异步
console.log(100)
setTimeout(function() {
  console.log(200)
}, 1000)
console.log(300)

// 同步
console.log(100)
alert(200)
console.log(300)

异步不会阻塞代码执行

同步会阻塞代码执行

应用场景

  • 网络请求,如 ajax 图片加载
  • 定时任务,如 setTimeout

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 = 'www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png'
loadImg(url).then(img => {
    console.log(img.width)
    return img
}).then(img => {
    console.log(img.height)
}).catch(ex => console.error(ex))

状态

  • 三种状态 pending resolved rejected 变化: pending => resolved 或 pending => rejected 变化是不可逆的
  • 状态的表现和变化 pending 状态,不会触发 then 和 catch resolved 状态,会触发后续的 then 回调函数 rejected 状态,会触发后续的 catch 回调函数
const p1 = new Promise((resolve, reject) => {})
console.log(p1) // pending

const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve()
  })
})
console.log(p2) // pending 一开始打印时
setTimeout(() => console.log(p2)) // resolved

const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject()
  })
})
console.log(p3) // pending 一开始打印时
setTimeout(() => console.log(p3)) // rejected

// 直接获取一个 resolved 状态的 Promise
const resolved = Promise.resolve(100)
console.log(resolved) // resolved

// 直接获取一个 rejected 状态的 Promise
const resolved = Promise.reject('err')
console.log(resolved) // rejected
  • then 和 catch 对状态的影响 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)
})

event-loop

  • JS 是单线程运行的
  • 异步要基于回调来实现
  • event loop 就是异步回调的实现原理

JS 如何执行的

  • 从前到后,一行一行执行
  • 如果某一行执行报错,则停止下面代码的执行
  • 先把同步代码执行完,再执行异步

event loop 执行过程

  • 同步代码,一行一行放在 Call Stack 执行
  • 遇到异步,会先 “记录” 下,等待时机(定时,网络请求)
  • 时机到了,就移动到 Callback Queue
  • 如果 Call Stack 为空(即同步代码执行完)Event Loop 开始工作
  • 轮询查找 Callback Queue ,如果有则移动到 Call Stack 执行
  • 然后继续轮询查找(永动机一样)

DOM 事件和 event loop

  • 异步(setTimeout,ajax 等)使用回调,基于 event loop
  • DOM 事件也使用回调,基于 event loop

async/await

  • 异步回调 callback hell
  • Promise 基于 then catch 链式调用,但也是基于回调函数
  • async/await 是同步语法,彻底消灭回调函数

有很多 async 的面试题,例如

  • async 直接返回,是什么
  • async 直接返回 promise
  • await 后面不加 promise

基本语法

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 = 'www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.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 () {
    // 注意:await 必须放在 async 函数中,否则会报错
    try {
        // 加载第一张图片
        const img1 = await loadImg1()
        console.log(img1)
        // 加载第二张图片
        const img2 = await loadImg2()
        console.log(img2)
    } catch (ex) {
        console.error(ex)
    }
})()

async/await 和 promise 的关系

  • async/await 是消灭异步回调的终极武器
  • 但和 promise 并不互斥
  • 两者反而相辅相成
  • await 相当于 promise 的 then
  • try...catch 可捕获异常,代替了 promise 的 catch
  • async 函数返回结果都是 Promise 对象(如果函数内没返回 Promise ,则自动封装一下)
async function fn2() {
    return new Promise(() => {})
}
console.log( fn2() )

async function fn1() {
    return 100
}
console.log( fn1() ) // 相当于 Promise.resolve(100)
  • 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 失败

异步本质

await 是同步写法,但本质还是异步调用。

async function async1 () {
  console.log('async1 start')
  await async2()
  console.log('async1 end') // 关键在这一步,它相当于放在 callback 中,最后执行
}

async function async2 () {
  console.log('async2')
}

console.log('script start')
async1()
console.log('script end')

即,只要遇到了 await ,后面的代码都相当于放在 callback 里。

for...of

  • for ... in (以及 forEach for)是常规的同步遍历
  • for ... of 常用与异步的循环
// 定时算乘法
function multi(num) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(num * num)
        }, 1000)
    })
}

// // 使用 forEach ,是 1s 之后打印出所有结果,即 3 个值是一起被计算出来的
// 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()

微任务/宏任务

  • 宏任务:setTimeout setInterval DOM 事件
  • 微任务:Promise(对于前端来说)
  • 微任务比宏任务执行的更早
console.log(100)
setTimeout(() => {
    console.log(200)
})
Promise.resolve().then(() => {
    console.log(300)
})
console.log(400)
// 100 400 300 200

event loop 和 DOM 渲染

  • 每一次 call stack 结束,都会触发 DOM 渲染(不一定非得渲染,就是给一次 DOM 渲染的机会!)
  • 然后再进行 event loop
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
            .append($p1)
            .append($p2)
            .append($p3)

console.log('length',  $('#container').children().length )
alert('本次 call stack 结束,DOM 结构已更新,但尚未触发渲染')
// (alert 会阻断 js 执行,也会阻断 DOM 渲染,便于查看效果)
// 到此,即本次 call stack 结束后(同步任务都执行完了),浏览器会自动触发渲染,不用代码干预

// 另外,按照 event loop 触发 DOM 渲染时机,setTimeout 时 alert ,就能看到 DOM 渲染后的结果了
setTimeout(function () {
    alert('setTimeout 是在下一次 Call Stack ,就能看到 DOM 渲染出来的结果了')
})

宏任务和微任务的区别

  • 宏任务:DOM 渲染后再触发
  • 微任务:DOM 渲染前会触发
// 修改 DOM
const $p1 = $('<p>一段文字</p>')
const $p2 = $('<p>一段文字</p>')
const $p3 = $('<p>一段文字</p>')
$('#container')
    .append($p1)
    .append($p2)
    .append($p3)

// // 微任务:渲染之前执行(DOM 结构已更新)
// Promise.resolve().then(() => {
//     const length = $('#container').children().length
//     alert(`micro task ${length}`)
// })

// 宏任务:渲染之后执行(DOM 结构已更新)
setTimeout(() => {
    const length = $('#container').children().length
    alert(`macro task ${length}`)
})

再深入思考一下:为何两者会有以上区别,一个在渲染前,一个在渲染后?

  • 微任务:ES 语法标准之内,JS 引擎来统一处理。即,不用浏览器有任何关于,即可一次性处理完,更快更及时。
  • 宏任务:ES 语法没有,JS 引擎不处理,浏览器(或 nodejs)干预处理。

经典面试题

async function async1 () {
  console.log('async1 start')
  await async2() // 这一句会同步执行,返回 Promise ,其中的 `console.log('async2')` 也会同步执行
  console.log('async1 end') // 上面有 await ,下面就变成了“异步”,类似 cakkback 的功能(微任务)
}

async function async2 () {
  console.log('async2')
}

console.log('script start')

setTimeout(function () { // 异步,宏任务
  console.log('setTimeout')
}, 0)

async1()

new Promise (function (resolve) { // 返回 Promise 之后,即同步执行完成,then 是异步代码
  console.log('promise1') // Promise 的函数体会立刻执行
  resolve()
}).then (function () { // 异步,微任务
  console.log('promise2')
})

console.log('script end')

// 同步代码执行完之后,屡一下现有的异步未执行的,按照顺序
// 1. async1 函数中 await 后面的内容 —— 微任务
// 2. setTimeout —— 宏任务
// 3. then —— 微任务



原创作者:Shadowbringer
链接:https://www.jianshu.com/p/17a4a041dc0f

Tags:

最近发表
标签列表