【前端面试】—— JavaScript(二)

ES6 数组新增了哪些扩展?

  • 扩展运算符 —— 将数组转换成参数序列

// 函数调用时将数组变成参数序列
function sum(arr, ...rest) {console.log(rest)    // [4, 5, 6]console.log(...rest)    // 4 5 6
}
function add(x, y) {console.log(x + y)return x + y
}sum([], 4, 5, 6)
add(...[1, 2])// 将某些数据结构转为数组
const divs = [...document.querySelectorAll('div')]// 数组复制(浅拷贝)
const a1 = [1, 2, 3]
const a2 = [...a1]// 数组合并(浅拷贝)
const a1 = ['a', 'b']
const a2 = ['c']
const a3 = ['d', 'e']
console.log([...a1, ...a2, ...a3]
)    // [ 'a', 'b', 'c', 'd', 'e' ]// 字符串拆分
[...'Hello']    // [ 'H', 'e', 'l', 'l', 'o' ]
  • 构造函数新增方法

// Array.from 将类数组和可遍历对象转换成真正的数组
const arrLike = {0: 'a',1: 'b',2: 'c',length: 3
}
const arr = Array.from(arrLike)    // [ 'a', 'b', 'c' ]// Array.of 创建一个具有可变数量参数的新数组实例
Array.of(5, 4, 3, 2, 1)    // [ 5, 4, 3, 2, 1 ]
  • 实例对象新增方法

find() findIndex()

ES6 对象新增了哪些扩展?

  • 属性简写

对象键名与值名相同时,可以简写

const name = 'thinc'
const obj = {name,// 等价于 name: namesayHi() { console.log(`Hi!`) },// 等价于// sayHi: function () { console.log(`Hi!`) }
}
  • 属性名表达式

可以将表达式放在括号内作为键使用

const thinc = 'a man'
const obj = {[thinc]: 'handsome'
}console.log(obj['a man'])    // handsome
console.log(obj[thinc])    // handsome
  • super 关键字

this 关键字指向函数所在的当前对象,super 指向当前对象的原型对象

const proto = {foo: 'hello'
};const obj = {foo: 'world',find() {return super.foo;}
};Object.setPrototypeOf(obj, proto); // 为obj设置原型对象
obj.find() // "hello"
  • 属性的遍历

遍历对象的属性都无法直接获取 Symbol 属性
- for...in:对象自身和继承的可枚举属性的键名
- Object.keys(obj):返回数组,包含自身的可枚举属性的键名
- Object.getOwnPropertyNames(obj):返回数组,包含自身所有属性(包括不可枚举属性)的键名
- Object.getOwnPropertySymbols(obj): 返回数组,包含自身所有 Symbol 属性的键名
- Reflect.ownKeys(obj):返回数组,包含对象自身所有键名()

  • 对象新增的方法

    • Object.is():严格判断两个值是否相等(===),有两个特殊情况(-0 和 +0 为 false;NaN 和 NaN 为 true)
    • Object.assign(target, ...source):将源对象的所有可枚举属性和值复制到目标对象
    • Object.getOwnPropertyDescriptors(obj):返回指定对象所有自身属性的描述对象
    • Object.setPrototypeOf(obj, prototype):设置对象的原型对象
    • Object.getPrototypeOf(obj):读取对象的原型对象
    • Object.entries():返回数组,包含对象自身可枚举的键值对

ES6 函数新增扩展

  • 参数默认值

function func({ a, b = 5} = {}) {console.log(a, b);
}func()    // undefined 5
func({ a: 1 })    // 1 5
func({ b: 2 })    // undefined 2
  • length 属性

返回没有指定默认值的参数个数,rest 参数不会计入 length 属性

  • 箭头函数

注意点:①不存在 arguments 对象,如需使用可以用 rest 参数代替;②不能作为 Generator 函数

// 返回对象需要加括号
const func = () => ({ name: 'thinc', age: 22 })

讲一讲 Set 和 Map

  • Set

Set 是 ES6 新增的数据结构,最大的特点是存放的值唯一。
增删查 可以用 add() delete() has() clear() 方法;
遍历可以用 values() forEach() 方法;
查看大小可以用 size 属性

  • Map

Map 类型是键值对的有序列表,键值可以是任意类型。
增删查改 可以用 set() delete() get() has() clear() 方法;
遍历可以用 keys() values() entries() forEach() 方法;
查看大小可以用 size 属性

  • WeakSet

只能添加 对象,不能添加原始值。如果没有其他对这个对象的引用,该对象会被从内存中自动清除。
不支持迭代,只有三种方法 add delete() has()

  • WeakMap

键必须是对象,不能是原始值。如果没有其他对这个对象的引用,该对象会被从内存中自动清除。
不支持迭代,只有四种方法 set() get() has() delete()
使用场景:额外数据的存储。在处理其他模块对象的时候,模块中的数据应该和对象共存亡;在网页的 DOM 元素上添加数据,当元素被清除,对应的 WeakMap 记录就会被自动移除;甚至可以作为缓存,当对象被垃圾回收时,对应的缓存结果也会被清除

// 📁 cache.js
let cache = new WeakMap()function process(obj) {if (!cache.has(obj)) {let res = /* complex computed for */ objcache.set(obj, res)}return cache.get(obj)
}// 📁 main.js
let obj = { /* some object */ }
let result1 = process(obj)
let result2 = process(obj)    // from cacheobj = null

说一说 Promise

Promise 是异步编程的一种解决方案,比传统的回调函数更合理。

  • 链式操作降低编码难度
  • 代码可读性增强

三种状态

promise 对象有三种状态:pending、fulfilled、rejected

then(fulfilled, rejected) 是状态发生改变时的回调函数,第一个参数接收 resolved 状态,第二个函数接收 rejected 状态

catch() 是专门用来捕获错误的方法,当然你也可以用 then(null, rejected)

finally() 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作

构造函数方法

  • Promise.all:并行执行多个 promise,等待所有 promise 都准备就绪,返回新的 promise。如果任意一个 promise 被 reject,返回的 promise 就会被立即 reject,并且带有的就是这个 error。
  • Promise.allSettled:等待所有的 promise 都被 settle,无论结果如何都返回成功或失败的数组。
  • Promise.race:只等待第一个 settled 的 promise 并获取其结果
  • Promise.resolve(value):用结果 value 创建一个 resolved 的 promise
  • Promise.reject(error):用 error 创建一个 rejected 的 promise

使用场景

图片加载

const url = 'https://ai-studio-static-online.cdn.bcebos.com/dcc139c7c72441b6989f9075bae9d6ab291d226cd4ca4224a795f2ddb5ccb164'function loadImage(src) {console.log(`2 - 函数初始化`);return new Promise((resolve, reject) => {const img = document.createElement('img')img.onload = () => {resolve(img)}img.onerror = () => {reject(new Error('图片加载失败 ${src}'))}img.src = src})
}console.log(`1 - 主线程代码`);loadImage(url).then(img => {console.log(`${img.width}x${img.height}`)
}).catch(err => {console.log(err)
})

说一说 Generator

generator 是一个函数,可以按需返回多个值。调用 next() 方法能获取最近的 yield 值,返回的结果是具有 value 和 done 属性的对象。
该对象具有 Symbol.iterator 属性,可以用 for…of 遍历。

function* gen() { /* some code */ }
const g = gen()
g[Symbol.iterator]() === g    // true

在异步编程中,可以用 yield 暂停函数执行,next 方法恢复函数执行

function loadImage(src) {console.log(`successful loading from ${src}`);
}const gen = function* () {console.log(`init...`);const img1 = yield loadImage('./icon-1.png');console.log(`param1: ${img1}`);const img2 = yield loadImage('./icon-2.png');console.log(`param2: ${img2}`);
};const g = gen()g.next()
console.log(`---`);
g.next('width=256px;height=256px;')
console.log(`---`);
g.next('width=28px;height=28px;')

说一说 Proxy

一个 Proxy 对象包装另一个对象并拦截诸如读取/写入属性和其他操作,可以选择自行处理它们,或者透明地允许该对象处理他们。

图片参考:Proxy 和 Reflect
在这里插入图片描述

Reflect 是一个内建对象,可简化 Proxy 的创建。若需要在 Proxy 内部调用对象的默认方法,建议使用 Reflect,其是 ES6 中操作对象而提供的新 API

let user = {}
Reflect.set(user, 'name', 'thinc')
console.log(user)    // { name: 'thinc' }

对于每个可被 Proxy 捕获的内部方法,在 Reflect 中都有一个对应的方法,其名称和参数与 Proxy 捕捉器相同。

说一说 Module

为什么需要模块

  • 代码抽象
  • 代码封装
  • 代码复用
  • 依赖管理

如果没有模块化,我们代码会怎样?

  • 变量和方法不容易维护,容易污染全局作用域
  • 加载资源的方式通过script标签从上到下。
  • 依赖的环境主观逻辑偏重,代码较多就会比较复杂。
  • 大型项目资源难以维护,特别是多人合作的情况下,资源的引入会让人崩溃

如何使用

在模块内部使用 export 关键字输出某个变量,使用 import 关键字加载模块

// a.js
export const user = {name: 'thinc',age: 23
}// b.js
import { user } from './a.js'

如果只有一个模块名,需要有配置文件,告诉引擎模块的位置

import { myMethod } from 'util'

还可以根据需要动态加载模块,将 import() 作为函数调用,其参数是模块的路径,返回 promise

import('path/to/module.js').then((module) => {// do something with the module})

JS 类型转换

显示转换

  • Number
  • parseInt
  • String
  • Boolean

涉及对象的转换会调用内置的 Symbol.toPrimitive 属性,其参数 hint 表示要转换到的原始值的预期类型

const user = {name: 'thinc',[Symbol.toPrimitive](hint) {if (hint === 'number') return 666else if (hint === 'string') return 'user'// 默认属性return true}
}console.log(+user)    // 666
console.log(`${user}`)    // user
console.log(user + '')    // true

隐式转换

在比较运算和算术运算以及 if while 需要布尔值的地方会发生隐式转换。

布尔值的转换

undefined null false +0 -0 NaN '' 会被转换成 false
有内容的字符串和任何非 null 对象都会被转换成 true

字符串的转换

String(1)    // 1
String(true)    // true
String(false)    // false
String({})    // [object Object]
String([])    // 
String(function () { })    // function () { }
String(undefined)    // undefined
String(null)    // null

数值的转换

除了 + 可能把算子转换成字符串,其他运算符都会把算子转换成数值。

'5' - '2'       // 3
'5' * '2'       // 10
true - 1        // 0
false - 1       // -1
'1' - 1         // 0
'5' * []        // 0
false / '5'     // 0
'abc' - 1       // NaN
null + 1        // 1
undefined + 1   // NaN

说一说闭包

概念

一个函数和对其词法环境的引用捆绑在一起,这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

使用场景

  • 创建私有变量
  • 延长变量生命周期

一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的

柯里化实现

function sum(a) {let currentSum = afunction f(b) {currentSum += breturn f}f.toString = () => { return currentSum }return f
}console.log(sum(1)(2)(3).toString()
)// 每个函数允许任意多个参数版本
function sum() {let nums = Array.prototype.slice.call(arguments)function f() {let rest = Array.prototype.slice.call(arguments)nums = nums.concat(rest)return f}f.toString = () => {return nums.reduce((pre, cur) => {return pre + cur})}return f
}console.log(sum(1, 3)(5)(7, 9).toString()
);

闭包对性能有一定影响,在某些情况下应该避免

function People(name, age) {this.name = namethis.age = agethis.sayHi = function() {console.log(`Hi~${this.name}`);}
}// 在使用构造函数时,方法应该关联对象的原型,避免每个对象的创建都会调用方法
function People(name, age) {this.name = namethis.age = age
}People.prototype.sayHi = function () {console.log(`Hi~${this.name}`);
}

JS 如何实现继承

原型链继承

将父类的实例作为子类的原型,通过隐藏的 __proto__ 实现继承

function Animal(name) {this.name = name || 'Animal'this.foot = [2, 4]
}function Cat() { }Cat.prototype = new Animal()
Cat.prototype.name = 'cat'const animal = new Animal()
const cat = new Cat()console.log(animal.name)    // Animal
console.log(cat.name)    // cat

缺点:
1. 如果创建多个实例,会导致共享一个原型对象;
2. 无法实现多继承

const cat1 = new Cat()
const cat2 = new Cat()cat1.foot.push(6)console.log(cat1.foot, cat2.foot)    // 都是 [2, 4, 6]

构造函数继承

使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类

function Cat(name) {Animal.call(this)this.name = name || 'Animal'
}const cat = new Cat('Tom')
console.log(cat);

缺点:
1. 只能继承父类的属性和方法,不能继承父类原型对象的属性或方法
2. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

组合继承

将前两种方法结合起来—— call + prototype。唯一的不足就是 Animal 执行了两次,造成了多构造一次的性能开销。

function Animal(name) {this.name = name || 'Animal'this.foot = [2, 4]
}function Cat(name) {// 第二次调用 AnimalAnimal.call(this, name)
}
// 第一次调用 Animal
Cat.prototype = new Animal()
Cat.prototype.constructor = Catconst cat = new Cat('Tom')
const cat2 = new Cat('Blue')
cat.foot.push(6)console.log(cat.foot, cat2.foot)

原型式继承

借助 Object.create 实现普通对象的继承 —— 使用现有的对象来提供新创建的对象的 proto。实现的是浅拷贝,存在篡改的可能。

const parent = {name: 'Parent',friends: ['f1', 'f2']
}let copy = Object.create(parent)copy.name = 'Copy'
copy.friends.push('f3')console.log(copy.name, copy.friends)    // Copy [ 'f1', 'f2', 'f3' ]
console.log(parent.name, parent.friends)    // Parent [ 'f1', 'f2', 'f3' ]

寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回这个对象

function 

寄生组合式继承

通过寄生方式,砍掉父类的实例属性,这样在调用两次父类的构造的时候,就不会初始化两次实例属性/方法,避免了组合继承的缺点

function Animal(name) {this.name = name || 'Animal'this.foot = [2, 4]
}function Cat(name) {Animal.call(this, name)
}function clone(parent, child) {child.prototype = Object.create(parent.prototype)child.prototype.constructor = child
}clone(Animal, Cat)const cat = new Cat()
const cat2 = new Cat('Tom')cat.foot.push(6)
console.log(cat)    // { "name": "Animal", "foot": [ 2, 4,  6 ] }
console.log(cat2)    // { "name": "Tom", "foot": [ 2, 4 ] }

说一说 this

this 关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象。

绑定规则

  • 全局环境定义的函数,this 指向 window(严格模式 this 指向 undefined)
  • 方法调用,this 指向调用者
  • 通过 new 关键字生成的实例对象,this 指向该实例对象(构造函数 return 对象则指向对象,null 除外)
  • 借助 apply/call/bind 显式修改
  • 箭头函数 this 指向最近一层非箭头函数的 this,否则指向全局对象
function F() {this.user = 'thinc'return null
}const a = new F()
a.user    // thinc

说一说执行上下文和执行栈

执行上下文:是一种对 JavaScript 代码执行环境的抽象概念,只要有 JavaScript 代码运行,它就一定在执行上下文中。
执行栈:具有 LIFO 结构,用于存储在代码执行期间创建的所有执行上下文。

说一说 JS 事件模型

  • 原始事件模型
    在 HTML 中直接绑定或 JS 绑定;只支持冒泡,不支持捕获;同类型事件只能绑定一次。
<input type='button' onclick="func()">
const btn = document.querySelect('.btn')
btn.onclick = func
  • 标准事件模型
    包括事件捕获、事件处理和事件冒泡阶段
btn.addEventListener(eventType, handler, userCapture)

typeof 与 instanceof 区别

  • typeof 返回变量基本类型,instanceof 返回布尔值
  • typeof 判断简单数据类型和函数,instanceof 判断引用数据类型

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上

如果需要通用检测数据类型,可以用 Object.prototype.toString,返回格式 "[object xxx]" 的字符串

说一说事件委托

通俗来讲,把一个元素响应事件委托给另一个元素执行。当执行到目标元素时,通过冒泡机制触发外层元素的绑定事件,在外层元素上执行函数。

适合事件委托的事件:click、mousedown、mouseup、keydown、keyup、keypress
优点:

  1. 减少绑定事件的内存消耗,提升性能
  2. 动态绑定,减少重复工作(每次新增元素需要新增绑定,删除需要解绑)
    局限:
  3. focus、blur 这些事件没有事件冒泡机制,无法进行委托绑定
  4. mousemove、mouseout 虽然有冒泡,但是需要不断计算位置,性能开销大,也不适合事件委托

说说 new 操作符具体干了什么

  1. 创建一个新对象 newObj
  2. 将新对象与构造函数的原型对象通过原型链连接起来
  3. 将构造函数中的 this 绑定到新对象 newObj 上
  4. 根据构造函数返回值做判断,如果是原始值则,如果对象则返回,也可以没有
    在这里插入图片描述
    手写 new 操作符
function _new(Func, ...rest) {// 1. 创建新对象// 2. 新对象原型指向构造函数原型对象const obj = Object.create(Func.prototype)// 3. 构造函数 this 指向新对象const res = Func.apply(obj, rest)// 4. 根据返回值判断return res instanceof Object ? res : obj
}


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部