跳至主要內容

JavaScript

zfh大约 21 分钟约 6243 字

ES6 新增那些东西?

  1. let 和 const 关键字
  2. 箭头函数
  3. 模板字符串
  4. 解构赋值
  5. 扩展运算符
  6. Promise 对象
  7. 类和继承
  8. 模块化
  9. 简化对象属性定义

说说 var、let、const 之间的区别

  • 变量提升: var 会提升的变量的声明到当前作用域的顶部 let const 不会

  • 暂时性死区:在代码块内,使用 let、const 命令声明变量或常量之前,该变量或常量都是不可用的。这在语法上,称为“暂时性死区”

  • 块级作用域:var 没有块级作用域,let const 有块级作用域

  • 重复声明: var 允许重复声明,let,const 不允许不允许在相同作用域内,重复声明同一个变量

  • 修改声明的变量: var 和 let 可以修改;const 声明一个只读的常量,并且 const 声明时,必须立即初始化

  • window 对象的属性和方法:全局作用域中,var 声明的变量和通过 function 声明的函数,会自动变成 window 对象的属性或方法,let、 const 不会

null 和 undefined 的区别

  • undefined 表示"缺少值",就是此处应该有一个值,但是还没有定义,null 表示"没有对象",即该处不应该有值

  • undefined 转为数值为 NaN,null 转为数值为 0

为什么 0.1+0.2!==0.3

JavaScript 是以 64 位双精度浮点数存储所有 Number 类型值,按照 IEEE754 规范,0.1 的二进制数只保留 52 位有效数字,即

;(1.100110011001100110011001100110011001100110011001101 * 2) ^ -4

同理,0.2 的二进制数为

;(1.100110011001100110011001100110011001100110011001101 * 2) ^ -3
复制代码

这样在进制之间的转换中精度已经损失。运算的时候如下

 0.00011001100110011001100110011001100110011001100110011010
+0.00110011001100110011001100110011001100110011001100110100
------------------------------------------------------------
=0.01001100110011001100110011001100110011001100110011001110

那么如何让其相等呢?

  • 将数字转成整数:
function add(num1, num2) {
  const num1Digits = (num1.toString().split('.')[1] || '').length
  const num2Digits = (num2.toString().split('.')[1] || '').length
  const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits))
  return (num1 * baseNum + num2 * baseNum) / baseNum
}
const Decimal = require('decimal.js');

const x = new Decimal(0.2)
console.log(x.plus(0.1)) //0.3
  • 为什么不是toFixedopen in new window

    toFixed在一些情况下,会出现和预期不符的结果:

    const r1=1.1+0.35
    const r2=1.1+0.45
    console.log(r1.toFixed(1)) //1.4 四舍五入希望是1.5
    console.log(r2.toFixed(1)) //1.6
    // why?
    1.45.toPrecision(20) // toPrecision返回一个以指定精度表示该数字的字符串'1.4499999999999999556'
    

数据类型检测的方式有哪些

typeof

typeof 对于string,boolean,number,undefined,function,symbol等类型可正确判断

对于null,array,object判断结果均为 object

特殊的对于 null,null 不是一个对象,尽管 typeof null 输出的是 object,这是一个历史遗留问题,JS 最初为了性能使用低位存储变量的 类型信息 ,000 开头代表是对象,null 表示为全零,所以将它错误的判断为 object

instanceof

instanceof 代码形式为object instanceof constructor(object 是否是 constructor 的实例),该操作符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上(可用于判断引用类型,不能判断原始类型)

constructor <Badge text="不建议使用,原型可能被改变'/>

constructor有两个作用,一是判断数据的类型,二是对象实例通过constructor访问它的构造函数

console.log([].constructor === Array)

Object.prototype.toString.call()

数组

  • Object.prototype.toString.call(obj).slice(8,-1) === 'Array'
  • 原型链
obj.__proto__ === Array.prototype
  • Array.isArray(obj)
  • obj instanceof Array
  • Array.prototype.isPrototypeOf(obj)

何时使用 === 何时使用 ==

除了 == null 之外 其它地方一律用===

const obj = { a: 2 }
if (obj.b == null) {
  // 相当于 if(obj.b===null||obj.b===undefined)
  console.log('b')
}

聊聊 JavaScript 中的数据类型

目前, JavaScript 原始类型有六种,分别为:

  • Boolean
  • String
  • Number
  • Undefined
  • Null
  • Symbol(ES6 新增)

Symbol是 JavaScript 中一种基本数据类型,它表示一种独一无二的数据类型,可以用来作为对象属性名或其他需要唯一标识符的场合。每个Symbol都是独一无二的,即使两个Symbol的描述字符串相同,它们也是不相等的。

使用Symbol可以避免对象属性名的冲突,因为每个Symbol都是唯一的,不会被覆盖或修改。同时,Symbol也可以用来定义一些语言内部的特殊行为,比如实现迭代器或自定义对象的 toString()方法等。

ES10 新增了一种基本数据类型:BigInt

BigInt类型通常用于需要处理大数的场景。JavaScript 的 Number 类型可以表示的最大整数是 2 的 53 次方减 1,也就是 9007199254740991。如果需要处理更大的整数,则可以使用BigInt类型。

由于 JavaScript 中的Number类型采用的是 IEEE 754 标准的双精度浮点数,其中只有 53 位用于表示整数部分,因此它只能表示有限的整数范围,超出范围的数字会失去精度或溢出。例如,当你声明一个非常大的数字时,它可能会被转换成科学计数法,这样就会导致丢失精度。另外,在进行复杂的计算时,也可能会出现精度误差的问题。

因此,如果需要处理更大的整数,应该使用BigInt类型,而不是Number类型,以确保数值的精度和正确性。

一些使用的场景:

  1. 处理加密算法,例如 RSA 加密算法需要大整数运算。

  2. 处理与时间相关的操作,例如计算 Unix 时间戳,需要处理大于Number.MAX_SAFE_INTEGER的整数。

  3. 处理与金额相关的操作,例如处理货币时可能需要使用到大数运算。

  4. 处理精度要求极高的计算,例如处理大型物理模拟或复杂科学计算等。

. 处理大型 ID 或其他需要大整数的标识符

引用类型只有一种: Object

null 不是一个对象,尽管 typeof null 输出的是 object,这是一个历史遗留问题,JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,null 表示为全零,所以将它错误的判断为 object

原始值和引用值的区别

内存的分配不同

  • 原始值存储在栈中
  • 引用值存储在堆中,栈中存储的变量,是指向堆中的引用地址

访问机制不同

  • 原始值是按值访问
  • 引用值按引用访问,JavaScript 不允许直接访问保存在堆内存中的对象,在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象的值

复制变量时不同

  • 原始值:a=b;是将 b 中保存的原始值的副本赋值给新变量 a,a 和 b 完全独立,互不影响
  • 引用值:a=b;将 b 保存的对象内存的引用地址赋值给了新变量 a;a 和 b 指向了同一个堆内存地址,其中一个值发生了改变,另一个也会改变

比较变量时不同

  • 原始值:==比较值是否相等(先进行类型转换再确定操作数是否相等---引自 js 高级程序设计(第四版) P71),===不仅比较值是否相等,还会比较数据类型是否相同

  • 引用数据类型:不管是 == 还是 === ,都是比较内存地址是否相同,即比较是否都指向同一个对象

参数传递的不同

函数传参都是按值传递(栈中的存储的内容):原始值,拷贝的是值;引用值,拷贝的是引用地址

手写深拷贝

仅仅是解决了深复制的关键问题,还需要针对不同的数据类型进行完善,lodash的深拷贝针对不同的数据类型进行了处理,见深入浅出 loadash 深拷贝源码open in new window

copyObjs.push({ target, copyTarget: obj }) 这一步为什么要放在递归遍历之前进行

这一步需要放在递归遍历之前进行,是因为在递归遍历中可能会遇到循环引用的情况,如果不先将新对象或数组保存在 copyObjs 中,那么在处理循环引用时就无法判断当前对象是否已经被拷贝过了,从而可能会陷入无限递归的情况。

举个例子,假设有一个对象 A,其中包含一个属性是指向自己的引用,即 A 的某个属性的值是 A 自身。如果不先将 A 拷贝后的副本保存在 copyObjs 中,那么在递归遍历 A 时会陷入无限递归的情况,因为递归到指向自身的属性时会一直循环下去。

而如果先将 A 拷贝后的副本保存在 copyObjs 中,那么在递归遍历 A 时就可以判断当前对象是否已经被拷贝过了,从而避免陷入无限递归的情况。因此,这一步需要放在递归遍历之前进行。

简易实现
function deepClone(obj = {}) {
  if (typeof obj !== 'object' || obj == null) {
    return obj
  }
  //    初始化返回结果
  let result = Array.isArray(target) ? [] : {}

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 保证key不是原型的属性
      result[key] = deepClone(obj[key])
    }
  }
  // 返回结果
  return result
}

JSON.sringify 和 JSON.parse 方法拷贝的缺陷

这是 JS 实现深拷贝最简单的方法了,原理就是先将对象转换为字符串,再通过 JSON.parse 重新建立一个对象。 但是这种方法的局限也很多:

  • 不能复制 function、正则、Symbol
  • 循环引用(当对象 1 中的某个属性指向对象 2,对象 2 中的某个属性指向对象 1 就会出现循环引用)报错
  • 相同的引用会被重复拷贝
let obj = { asd: 'asd' }
let obj2 = { name: 'aaaaa' }
obj.ttt1 = obj2
obj.ttt2 = obj2
let cp = JSON.parse(JSON.stringify(obj))
obj.ttt1.name = 'change'
cp.ttt1.name = 'change'
console.log(obj, cp)

对于上面的代码,原对象改变 ttt1.name 也会改变 ttt2.name ,因为他们指向相同的对象。但是,复制的对象中,ttt1ttt2 分别指向了两个对象。拷贝的对象没有保持和原对象一样的结构。因此,JSON 实现深拷贝不能处理指向相同引用的情况,相同的引用会被重复拷贝

如何用 class 实现继承

利用es6extends实现继承没难度,可参考《JS高级程序设计》es5实现继承的方式

如何理解 JS 原型(隐式原型和显式原型)和原型链

原型和实例的关系:每个构造函数都有一个原型对象,原型有 一个属性指回构造函数,而实例有一个内部指针指向原型

JS高级程序设计》P238

JavaScript 中是使用构造函数来新建一个对象的(或者使用 class 类实例化),每一个构造函数的内部都有一个 prototype 属性,这个就是显式原型,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个叫做__proto__的属性,这个属性指向构造函数的 prototype 属性对应的值,这个就是隐式原型__proto__

fib-原型链关系
fib-原型链关系

ES5 中新增了一个 Object.getPrototypeOf()方法,可以通过这个方法来获取对象的原型

原型链查找

当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。所以这就是新建的对象为什么能够使用 toString() 等方法的原因

原型链的终点是null,因为 Object.prototype.__proto__指向 null

手写一个简易的 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++) {
      fn(this[i])
    }
  }
  on(type, fn) {
    this.each((elem) => {
      elem.addEventListener(type, fn, false)
    })
  }
}
const $ = new Jquery('p')
console.log($.get(1))
$.on('click', () => {
  alert('123')
})
Jquery.prototype.addClass = function (index, className) {
  this.get(index).classList.add(className)
}
$.addClass(2, 'wocainima')
//   -----------------------------
class JqueryPlus extends Jquery {
  constructor(selector) {
    super(selector)
  }
  changeStyle(index, key, value) {
    this.get(index).style[key] = value
  }
}
const _ = new JqueryPlus('p')
_.changeStyle(0, 'fontSize', '20px')
_.changeStyle(0, 'color', 'red')

简述一下 new 的过程

1)在内存中创建一个新对象

2)将新对象与构造函数通过原型链连接起来

3)将构造函数中的this 绑定到新对象上

4)执行构造函数内部的代码

5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

function Fun(a) {
  this.a = a
  return { [a]: 1 }
}
const obj = new Fun(2)
console.log(obj) // { '2': 1 }而不是 { a: 2 }

实现一个new方法

Function.prototype.myNew = function () {
  // 1)在内存中创建一个新对象
  // 2)将新对象与构造函数**通过原型链**连接起来
  const that = Object.create(this.prototype)
  // 3)将构造函数中的**this 绑定**到新对象上
  // 4)执行构造函数内部的代码
  const result = this.apply(that, arguments)
  // 5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
  return result instanceof Object ? result : that
}

什么是作用域?什么是自由变量?

词法作用域和动态作用域

  • 词法(静态)作用域: 在代码书写的时候完成划分,作用域链沿着它定义的位置往外延伸
  • 动态作用域: 在代码运行时完成划分,作用域链沿着它的调用栈往外延伸
var name = 'fan'

function showName() {
  console.log(name)
}

function changeName() {
  var name = 'hang'
  showName()
}

changeName() // 词法: fan 动态:hang

全局作用域

声明在任何函数之外的顶层作用域的变量就是全局变量,这样的变量拥有全局作用域

所有未定义直接赋值的变量拥有全局作用域

所有 window 对象的属性拥有全局作用域

全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突

函数作用域

在函数内部定义的变量称为局部变量,拥有函数作用域

只有函数被调用的时候才会形成函数作用域

内层作用域可以访问外层作用域,反之不行

块级作用域

使用 ES6 中新增的 let 和 const 指令可以声明块级作用域

块作用域内的变量只要出了自己被定义的那个代码块,那么就无法访问了。

在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部

自由变量

一个变量在当前作用域没有定义但被使用了

自由变量的查找规则:自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方,因为 js 是词法作用域

如何理解闭包

作用域应用的特殊情况,有两种情况:

  1. 函数作为参数被传递
function print(fn) {
  let a = 200
  fn()
}
let a = 100

function fn() {
  console.log(a)
}

print(fn)
  1. 函数作为返回值被返回
function create() {
  let a = 100
  return function () {
    console.log(a)
  }
}
let fn = create()
let a = 200
fn()

闭包代码输出问题

常见的循环体输出

破解前端面试(80% 应聘者不及格系列):从闭包说起open in new window

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i)
  }, 1000)
}

console.log(i) //5 5 5 5 5

如何让它按 1,2,3,4,5 输出?

  1. setTimeout 从第三个入参位置开始往后,是可以传入无数个参数的。这些参数会作为回调函数的附加参数存在。
for (var i = 0; i < 5; i++) {
  setTimeout(
    function (j) {
      console.log(j)
    },
    1000,
    i
  )
}
  1. 在 setTimeout 外面再套一层函数,利用这个外部函数的入参来缓存每一个循环中的 i 值:
var output = function (i) {
  setTimeout(function () {
    console.log(i)
  }, 1000)
}

for (var i = 0; i < 5; i++) {
  // 这里的 i 被赋值给了 output 作用域内的变量 i
  output(i)
}
  1. 在 setTimeout 外面再套一层函数,只不过这个函数是一个立即执行函数。利用立即执行函数的入参来缓存每一个循环中的 i 值:
for (var i = 0; i < 5; i++) {
  // 这里的 i 被赋值给了立即执行函数作用域内的变量 j
  ;(function (j) {
    setTimeout(function () {
      console.log(j)
    }, 1000)
  })(i)
}

讲一下闭包的使用场景

待扩展

创建 10 个 a,点击弹出对应的序号

<div id="root"></div>
const root = document.getElementById('root')
for (let i = 1; i <= 10; i++) {
  const a = document.createElement('a')
  a.innerText = i
  a.style.display = 'block'
  a.onclick = function (e) {
    e.preventDefault()
    alert(i)
  }
  root.appendChild(a)
}

手写 bind 函数

如何手写一个 bind 方法open in new window

提示

bind() 方法创建一个新的函数(这个包装了目标函数),在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用

  • bind 的用法
function fn1(a, b, c) {
  console.log('this', this)
  console.log(a, b, c)
  return 'this is fn1'
}
const fn2 = fn1.bind({ x: 100 }, 10, 20, 30)
const res = fn2()
console.log(res)
  • 手写 bind
// 剩余参数是ES6的新特性,一般说来,不支持bind的情况一般也不支持剩余参数,所以,不推荐这种写法
// 不能使用箭头函数 箭头函数不支持arguments
Function.prototype.bind1 = function () {
  // 将参数拆解为数组
  // call支持最低版本chorme-1
  const args = Array.prototype.slice.call(arguments)
  //   获取this,数组第一项
  const _this = args.shift()
  // 返回一个函数
  return () => {
    // apply支持最低版本chorme-1
    // bind支持最低版本chorme-7
    this.apply(_this, args)
  }
}
// ----------------------------------------
const obj = {
  a: 2,
  b() {
    console.log(this.a)
  },
}
const _obj = {
  a: 3,
  b() {
    console.log(this.a)
  },
}
obj.b.bind1(_obj)()

this 指向问题

  • 作为普通函数使用,指向window,严格模式下指向underfind
  • call apply bind call apply bind 三者的用法和区别open in new window ,指定 this(非箭头函数)
  • 作为对象方法(非箭头函数)被调用,指向当前对象
  • 箭头函数的this永远取它上层作用域的this
  • 延时器、定时器调用非箭头函数,指向window
  • 全局作用域下指向window
  • 事件处理函数(非箭头函数)的上下文是绑定事件的DOM元素

手写 promise 加载图片

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>promise加载图片</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="text/javascript">
      const URL = 'https://zfhblog.top/titlelogo.png'
      const root = document.getElementById('root')

      function loadImg(url) {
        return new Promise((resolve, reject) => {
          const img = document.createElement('img')
          // 加载完
          img.onload = () => {
            resolve(img)
          }
          // 加载失败
          img.onerror = () => {
            const err = new Error(`图片加载失败了,图片地址为${url}`)
            reject(err)
          }
          img.src = url
        })
      }
      loadImg(URL)
        .then((img) => {
          root.appendChild(img)
          console.log('我第二个执行then')
        })
        .catch((err) => {
          console.log(err)
        })
      console.log('我第一个执行,同步代码')
      setTimeout(() => {
        console.log('我第三个执行setTimeout')
      }, 0)
    </script>
  </body>
</html>

setTimeout 结果输出题

console.log(1)
setTimeout(function () {
  console.log(2)
}, 1000)
console.log(3)
setTimeout(function () {
  console.log(4)
}, 0)
console.log(5)

输出结果: 1 , 3 , 5 , 4 , 2

描述事件循环机制

浏览器event-loop
浏览器event-loop
  1. 执行一个宏任务(主代码块也属于宏任务)(执行栈中没有就从宏任务队列中获取)
  2. 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  3. 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  4. 尝试DOM渲染
  5. 开启下一轮事件循环

什么是宏任务什么是微任务,两者区别

  • 宏任务:setTimeout, setinterval , Ajax , DOM 事件(浏览器规定的)
  • 微任务:Promise(then),async/await(ES6 语法规定的)
  • 微任务的执行时机比宏任务早

Promise 的三种状态,如何变化

手写 Promise

是否满足Promise A+规范open in new window

  1. 初始化 & 异步调用
  2. 实例方法then catch 链式调用
  3. 静态方法:resolve reject all race any allSettled
EasyPromise
class EasyPromise {
  // promise 的 状态 pending fulfilled rejected
  PromiseState = 'pending'
  // promise 的 结果
  PromiseResult = null
  // pending 状态下保存的回调函数任务栈
  callBacks = []
  constructor(actuator) {
    // reslove处理函数
    const reslove = (result) => {
      if (this.PromiseState !== 'pending') return
      this.PromiseResult = result
      this.PromiseState = 'fulfilled'

      this.callBacks.forEach((fn) => {
        fn.onResloved()
      })
    }
    // reject处理函数
    const reject = (error) => {
      if (this.PromiseState !== 'pending') return
      this.PromiseResult = error
      this.PromiseState = 'rejected'

      this.callBacks.forEach((fn) => {
        fn.onRejected()
      })
    }
    try {
      actuator(reslove, reject)
    } catch (err) {
      reject(err)
    }
  }

  then(resloveCallback, rejectCallBack) {
    resloveCallback =
      typeof resloveCallback === 'function' ? resloveCallback : (v) => v
    rejectCallBack =
      typeof rejectCallBack === 'function'
        ? rejectCallBack
        : (e) => {
            throw e
          }
    return new EasyPromise((reslove, reject) => {
      // 判断返回值的类型
      const judgeReturnValueType = (callBack) => {
        try {
          const result = callBack(this.PromiseResult)
          if (result instanceof EasyPromise) {
            // 由返回的Promise的状态决定
            result.then(
              (v) => {
                reslove(v)
              },
              (e) => {
                reject(e)
              }
            )
          } else {
            reslove(result)
          }
        } catch (err) {
          reject(err)
        }
      }
      switch (this.PromiseState) {
        case 'fulfilled':
          // 一个微任务
          queueMicrotask(() => {
            judgeReturnValueType(resloveCallback)
          })
          break
        case 'rejected':
          queueMicrotask(() => {
            judgeReturnValueType(rejectCallBack)
          })
          break
        default:
          this.callBacks.push({
            onResloved() {
              judgeReturnValueType(resloveCallback)
            },
            onRejected() {
              judgeReturnValueType(rejectCallBack)
            },
          })
      }
    })
  }

  catch(rejectCallBack) {
    return this.then(null, rejectCallBack)
  }

  static all(promiseList = []) {
    return new EasyPromise((reslove, reject) => {
      const result = [] // 存储PromiseList结果的数组
      const length = promiseList.length
      let resloveCount = 0 // 结果为成功的个数
      promiseList.forEach((p) => {
        p.then((data) => {
          result.push(data)
          resloveCount++
          if (resloveCount === length) reslove(result)
        }).catch((err) => {
          reject(err)
        })
      })
    })
  }

  static any(promiseList = []) {
    return new EasyPromise((reslove, reject) => {
      let rejectCount = 0 // 结果为失败的个数
      const length = promiseList.length
      promiseList.forEach((p) => {
        p.then((data) => {
          reslove(data)
        }).catch(() => {
          rejectCount++
          if (rejectCount === length) {
            reject('All promises were rejected')
          }
        })
      })
    })
  }

  static race(promiseList = []) {
    return new EasyPromise((reslove, reject) => {
      promiseList.forEach((p) => {
        p.then((data) => {
          reslove(data)
        }).catch((err) => {
          reject(err)
        })
      })
    })
  }
  static allSettled(promiseList = []) {
    return new EasyPromise((reslove, reject) => {
      let result = []
      promiseList.forEach((p) => {
        p.then((data) => {
          result.push({ status: 'fulfilled', value: data })
        }).catch((err) => {
          result.push({ status: 'rejected', reason: err })
        })
      })
      reslove(result)
    })
  }
  static reslove(value) {
    return new EasyPromise((reslove, reject) => {
      reslove(value)
    })
  }
  static reject(reason) {
    return new EasyPromise((reslove, reject) => {
      reject(reason)
    })
  }
}

property 和 attribute 的区别

  • property:修改对象属性,不会提现到html结构中
  • attribute:修改html属性,会改变html结构
  • 两者都有可能引起DOM的重新渲染

如何优化 DOM 操作的性能

注意

涉及前端性能优化,此处答案过于简单,后期扩展

  1. 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 < document.getElementsByTagName('p').length; i++) {
  // 缓存length 只进行一次DOM查询
}
  1. 将频繁操作改为一次操作
const list = document.querySelector('.list')
// 文档切片
const frag = document.createDocumentFragment()
for (let i = 0; i < 10000; i++) {
  const title = document.createElement('h1')
  title.innerText = `list item ${i}`
  frag.appendChild(title)
}
list.appendChild(frag)

编写一个通用的事件监听函数

    const bindEvent = (elem, type, selector, fn) => {
      if (fn == null) {
        fn = selector
        selector = null
      }
      elem.addEventListener(type, (event) => {
        const target = event.target
        if (selector) {
          // 代理
          if (target.matches(selector)) {
            fn.call(target, event)
          }
        } else {
          // 普通
          fn.call(target, event)
        }
      })
    }

无限下拉的图片列表,如何监听每个图片的点击

  • 事件代理
  • event.target获取触发元素
  • 用``matches`来判断是否是触发元素

实现 add(1)(2)(3) 柯里化

Object.is()与比较操作符的区别

使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊情况,比如-0 和+0 不再相等,两个 NaN 是相等的

箭头函数与普通函数的区别

  1. 箭头函数没有自己的 this
  2. call、apply、bind 不会改变箭头函数的 this 指向
  3. 箭头函数不能作为构造函数使用
  4. 箭头函数没有 arguments
  5. 箭头函数没有 Prototype

简述 new 操作符的执行过程

1)在内存中创建一个新对象

2)将新对象与构造函数通过原型链连接起来

3)将构造函数中的this 绑定到新对象上

4)执行构造函数内部的代码

5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

function Fun(a) {
  this.a = a
  return { [a]: 1 }
}
const obj = new Fun(2)
console.log(obj) // { '2': 1 }而不是 { a: 2 }
上次编辑于:
本站勉强运行 小时