数组常用方法有哪些?
- 增
- unshift()
- push()
- 删
- shift()
- pop()
- 查
- indexOf() —— 返回要查找的元素在数组中的位置,如果没找到则返回 -1
- includes() —— 返回要查找的元素是否在数组中存在,如果不存在返回 false
- find(item, idx, arr) —— 返回第一个匹配的元素
- 改
- splice(startIndex, deleteCount, [array]) —— 从开始位置删除 m 个元素,并插入 n 个元素
- slice() —— 数组切片
- 排序
- reverse() —— 反转原数组
- sort() —— 排序(
arr.sort((a, b) => a - b)
升序)
- 转换
- join() —— 将数组按分隔符合并后返回字符串
- 迭代
- some(item, idx, arr) —— 对数组每一项运行传入函数,如果有一项返回 true 则整体返回 true
- every(item, idx, arr) —— 对数组每一项运行传入函数,如果都返回 true 则整体返回 true
- forEach(item, idx, arr) —— 对数组每一项运行传入函数,返回 undefined
- filter(item, idx, arr) —— 对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回
- map(item, idx, arr) —— 对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组
JS 字符串常用方法?
由于 JS 字符串创建后不能修改,所以部分操作和数组不太一样。
- 增
- 加法
- concat() —— 拼接多个字符串
- 删
- slice()
- 查
- indexOf() —— 返回指定字符串的索引
- includes(str, startIndex) —— 根据 str 中是否包含 substr 返回 true/false
- startsWith()
- endsWith()
- 改
- trim() —— 删除两边空格
- repeat()
- padStart(num, str) —— 在字符串开头/结尾把字符用 str 填充到 num 这么多
- padEnd(num, str)
- toLowerCase()
- toUpperCase()
- 模板匹配
- match() —— 返回匹配成功的字符串数组
- search() —— 返回匹配成功的索引,失败返回 -1
- replace()
typeof 能判断哪些类型
- 识别所有值类型
- 识别函数
- 判断是否是引用类型(不可再细分)
// 判断一个变量是否声明
typeof x !== undefined
何时使用 === 何时使用 ==
除了 == null 之外,其他一律用 ===
const obj = { x: 100 }
if (obj.a == null) {}
// 相当于
if (obj.a === null || obj.a === undefined) {}
值类型和引用类型的区别
- 值类型存储在栈中,引用类型存储在堆中;
- 值类型在栈中存放原始值,引用类型在栈中存放内存地址;(考虑到性能问题,值占用空间少)
const obj1 = { x: 100 }
const obj2 = obj1
let x1 = obj1.x
obj2.x = 101
x1 = 102
> obj1.x // 101
if 语句和逻辑运算
- truly 变量:
!!a === true
的变量 - falsely 变量:
!!a === false
的变量
作用域和闭包
- this 的不同应用场景,如何取值?
在函数执行的地方决定,而不是在定义的时候决定
- 手写 bind 函数
Function.prototype.myBind = function () {
// 将参数拆解为数组
const args = Array.prototype.slice.call(arguments)
// 获取 this(数组第一项)
const t = args.shift()
// fn1.bind(...) 中的 fn1
const self = this
// 返回一个函数
return function () {
return self.apply(t, args)
}
}
- 实际开发中闭包的应用场景,举例说明
在函数中的变量只能在函数内访问,自己来维护该变量。如果外部需要访问可以创建一些 api 。
作用域即变量的合法使用范围,分为全局作用域、函数作用域和块级作用域(ES6 新增)
闭包:访问一个变量时,是在函数定义的地方,向上级作用域查找,不是在执行的地方
- 作用域应用的特殊情况
- 函数作为参数被传递
- 函数作为返回值被返回
为什么输出都是 5 ?
let i
for (i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 0)
}
因为 i 的声明是在 for 之外,这里是全局作用域。当打印执行打印语句的时候已经退出 for 循环,i 的值为 5,所以打印的都是 5.
for (let i = 0; i < 5; i++) { ... }
这里 let 声明的变量都是块级作用域,因此会在每次循环中都声明一个新的 i 变量,也就不会导致刚才的问题。
也可以使用匿名函数创建一个闭包
let i
for (i = 0; i < 5; i++) {
(function (i) {
setTimeout(() => console.log(i), 0)
})(i)
}
单线程和异步
- 手写 Promise 加载一张图片
const url = ''
function loadImage(src) {
return new Promise((resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
reject(new Error('图片加载失败 ${src}'))
}
img.src = src
})
}
loadImage(url).then(img => {
console.log(`${img.width}x${img.height}`)
}).catch(err => {
console.log(err)
})
请描述 event loop 的机制
JS 是单线程运行的,异步要基于回调来实现,event loop 就是异步回调实现的原理。
var let const 的区别
- 作用域
var 只有函数和全局作用域,不存在块级作用域
let 和 const 是块级作用域,在 let 和 const 中声明的变量仅可在该块中使用。比如 if while for 。
- 用途
var 变量可以重新声明;
let 变量不能被重新声明,但是可以被修改;
const 常量不能被修改。
- 变量提升
声明的变量都会被提升到作用域顶部。
var 会使用 undefined 对其初始化
let 不会对值进行初始化。所以如果尝试在声明前访问 let 变量会出错
const 必须要初始化
JS 数据类型
- 划分
基本数据类型:number、bigint、string、boolean、null、undefined、symbol
引用数据类型:object
- 类型判断
typeof:识别所有基本类型,判断是否为引用类型,识别函数
instanceof:判断两边对象是否属于实例关系
原型和原型链
- 原型
JavaScript 是基于原型的,我们创建的每个函数都有一个 prototype(原型)
属性,这个属性是一个指针,指向一个对象(原型对象),这个对象存放可以让所有实例共享的属性和方法。
原型对象默认拥有一个 constructor
属性,指向指向它的那个构造函数
每个实例对象都拥有一个隐藏的属性 __proto__
,指向它的原型对象
实例自身属性会屏蔽原型上面的同名属性,实例上没有的属性回去原型上面找
在已经创建了实例的情况下重写原型,会切断现有实例与新原型之间的联系
重写原型对象,会导致原型对象的 constructor
属性指向 Object
,原型链关系混乱。所以我们应该在重写原型对象的时候指定 constructor
People.prototype = {
constructor: People,
// ...
}
- 原型链
JavaScript 中所有的对象都是由它的原型对象继承而来的。而原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链
所有原型链的终点都是 Object
函数的 prototype
属性
Object.prototype
指向的原型对象同样拥有原型,不过它的原型是 null
,而 null
没有原型
可以把实例看作构造函数的另一个原型对象,因为 Person.prototype.constructor === p.constructor
讲一讲浅拷贝和深拷贝
浅拷贝和深拷贝只针对对象和数组这样的引用数据类型。
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;
深拷贝会创建一个新的一模一样的对象,新对象跟原对象不共享内存,修改新对象不会影响原对象
- 浅拷贝实现方式
// Object.assign()
// 当 object 只有一层的时候,是深拷贝
const user = {
name: 'thinc',
age: 23
}
const user2 = Object.assign({}, user)
user2.age = 3
console.log(user) // {name: 'thinc', age: 23}
// Array.prototype.concat()
const arr = [1, 2, { name: 'thinc' }]
const arr2 = arr.concat()
arr2[0] = 111
console.log(arr) // [111, 2, { name: 'thinc' }]
// Array.prototype.slice()
const arr = [1, 3, { name: 'Thinc' }]
const arr2 = arr.slice()
arr2[1] = 233
console.log(arr) // [1, 233, { name: 'Thinc' }]
补充说明:Array 的 slice 和 concat 方法不修改原数组,只是返回了一个浅复制原数组中元素的新数组。如果元素是引用类型,只会复制对象的引用(指针);如果是基本数据类型,会把具体的值拷贝过来。
- 深拷贝实现方式
- 用
JSON.stringify
将对象转换成 JSON 字符串,再用JSON.parse
把字符串解析成对象。不能处理函数
// JSON.parse(JSON.stringify())
let user1 = {
age: 23,
city: {
now: 'Langfang',
ago: 'Wenzhou'
}
}
let user2 = JSON.parse(JSON.stringify(user1))
user2.age = 24
user2.city.now = 'Beijing'
console.log(user1) // { age: 23, city: { now: 'Langfang', ago: 'Wenzhou' }}
console.log(user2) // { age: 24, city: { now: 'Beijing', ago: 'Wenzhou' }}
- 函数库 lodash
const _ = require('lodash')
const obj1 = {
a: 1,
b: {
f: {
g: 2
}
}
}
const obj2 = _.cloneDeep(obj1)
obj2.a = 111
obj2.b.f.g = 222
console.log(obj1) // { a: 1, b: { f: { g: 2 } } }
console.log(obj2) // { a: 111, b: { f: { g: 222 } } }
- 手撕递归
/**
* 深拷贝
* @param {Object} obj
*/
function deepClone(obj = {}) {
if (typeof obj !== 'object' || obj == null) {
return obj
}
// 初始化返回结果
let res = obj instanceof Array ? result = [] : result = {}
for (let key in obj) {
// 保证 key 不是原型的属性
if (obj.hasOwnProperty(key)) {
// 递归!!!
res[key] = deepClone(obj[key])
}
}
return res
}
如何获取数组的最后一个元素?(arr[-1]是无效的)
- arr.pop() 删除并返回最后一个元素
- arr[arr.length - 1] 使用数组的length属性
- arr.slice(-1)[0] 使用JS的slice方法
如何保证异步请求执行顺序
需要发起异步请求,但是这个异步请求的参数也需要异步获得。现在的问题是getTjbd中的params不是处理过后的值
const params = {
imgs: form.fileList.map((item) => {
postBase64({ base64: item.url }).then((res) => {
item.url = res.data.url
item.thumbUrl = res.data.url
})
}
getTjbd(params)
.then((res) => {
this.$router.push({ path: '/goodsManage' })
this.$message.success('提交成功')
})
解决:使用函数式编程
将字符串重复 n 次
- 三元表达式+递归
function multiple(str, n) {
return n > 1 ? str += multiple(str, --n) : str
}
- 数组方法
function multiple2(str, n) {
return new Array(n + 1).join(str)
}
- ES6 字符串方法
function multiple3(str, n) {
return str.repeat(n)
}
JS 交换变量的方法
- 中间变量
- ES6 解构
- 加减法(针对数字)
- 异或运算(针对数字)
- 对象和数组
- try…catch
- 数组的两个值交换
如何用 js 手写一个 new 方法
首先搞明白 new 方法做了一件什么事?
function People(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
People.prototype.fullName = function () {
return `${this.firstName} ${this.lastName}`
}
const thinc = new People('first-name', 'last-name')
console.log(thinc);
简单来说就是隐式创建和返回了 this 对象
function People(firstName, lastName) {
// 隐式创建
// this = {}
this.firstName = firstName
this.lastName = lastName
// 隐式返回
// return this
}
具体点分为三步:
- 创建一个新对象,继承父类原型上的属性和方法
newObj.__proto__ = oldObj.prototype
- 添加父类(People)的属性和方法到新对象中
- 如果执行结果有返回值,则返回执行结果;否则返回新创建的对象
代码
function _new(obj, ...rest){
// 基于obj的原型创建一个新的对象
// 在 prototype 上定义公共属性和方法
const newObj = Object.create(obj.prototype);
// 添加属性到新创建的 newObj 上, 并获取 obj 函数执行的结果.
const result = obj.apply(newObj, rest);
// 如果执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象
return result instanceof Object ? result : newObj;
}
如何理解 this 关键字
- 方法中的 this,指向调用方法的对象
- 全局环境的 this 指向全局对象
- 全局函数其实是 window(全局对象) 的方法
function fun() {
console.log(this)
}
fun()
// 等价于 window.fun()
- 事件中的 this,指向触发事件的 DOM 对象
- 构造函数中的 this,指向 new 创建的对象(构造函数是用来创建对象的)
- 箭头函数没有 this
call / apply / bind
Function.prototype.call()
call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数
Function.prototype.myCall = function (context) {
if (typeof this !== 'function') return
const args = Array.prototype.slice.call(arguments, 1)
context.fn = this
const res = context.fn(...args)
delete context.fn
return res
}
function Production(name) {
this.name = name
}
function Food(name) {
Production.myCall(this, name)
this.category = 'food'
}
console.log(
new Food('cheese')
)
Function.prototype.apply()
apply() 方法使用一个指定的 this 值和一个数组(或类数组对象)来调用一个函数
说一说 Axios
Axios 是一个基于 promise 网络请求库,作用在 node.js 和浏览器中。其中 服务端 使用 node.js 原生 http 模块,客户端使用 XMLHttpRequests 。
特性:
- 从浏览器创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求和响应数据
- 取消请求
- 自动转换JSON数据
- 客户端支持防御XSRF
配置:
import axios from 'axios'
// 全局 axios 默认值
axios.defaults.baseURL = 'https://127.0.0.1:8000';
function get(obj) {
return axios({
method: 'GET',
url: obj.url || '', // { 'api/blog/list' }
headers: obj.headers || {}, // { 'Content-Type': 'application/json' }
params: obj.params || {}, // { id: 1 } 与请求一起的 url 参数
data: obj.data || {}, // { title: 'xxx' } 仅适用 'POST', 'PUT', 'DELETE 和 'PATCH' 请求方法
timeout: obj.timeout || 1000,
proxy: '' // 还没用过,暂时放着
})
}
// 添加请求拦截器
const interceptors1 = axios.interceptors.request.use(
// 在发送请求前做些什么
conf => {
const token = localStorage.getItem('Access-Token')
token && (conf.headers['token'] = token)
return conf
},
// 对请求错误做些什么
err => {
return Promise.reject(err)
}
)
// 添加响应拦截器
const interceptors2 = axios.interceptors.response.use(
res => {
// errMsg 是服务器定义的键
if (res.data.errMsg === '用户信息验证失败') {
// 提示错误原因
alert('用户信息验证失败')
// 跳转到对应页面
// router.push({ name: 'login' })
}
return conf
},
err => {
return Promise.reject(err)
}
)
// 移除拦截器
axios.interceptors.request.eject(interceptors1)
axios.interceptors.response.eject(interceptors2)
get({
url: 'api/blog/list'
})
说一说 cookie/localStorage/sessionStorage
Cookie
生命周期:由服务器设置有效时间
存储大小:4K 左右
通信:传输介质。每次都会携带在HTTP头中(如果使用cookie保存过多数据会带来性能问题)
应用场景:用户登录判断。在用户登陆后往 Cookie 中插入一段能唯一辨识用户身份的辨识码,下次只要读取这个值就可以判断用户是否登录
localStorage
生命周期:除非被清除,否则永久保存
存储大小:一般为 5M
通信:存储介质。仅在客户端保存,不参与服务器通信
应用场景:购物车管理
sessionStorage
生命周期:仅在当前会话下有效,关闭页面或浏览器后被清除(可以跨页面)
存储大小:一般为 5M
通信:仅在客户端保存,不参与服务器通信
localStorage 侧重于存储数据,Cookie 侧重于与服务器通信,并且存放数据相对更安全(可以设置有效时间)
说一说 require 和 import 的区别?
require | import | |
---|---|---|
规范 | CommonJS | ES6 |
使用限制(NodeJS) | 所有版本 | Node 9.0+(启动需加上 flag --experimental-modules)Node 13.2+(直接启动) |
使用限制(浏览器) | 不支持 | <script> 中包裹 type: module |
加载时间 | 运行时动态加载 | 静态编译 |
最直观的感受是导入导出语法结构不一样。require 直接把里面的文件赋值给一个变量,在导出的时候用 module.exports
指定内容;import 有其固定的语法,import moduleName from module
,导出的时候用 export
关键字指定内容。
然后就是使用场景了。require 一般用在 nodejs 中,在浏览器需要用 webpack 进行转换;import 可以直接在 node 13.2+ 版本中使用,浏览器中需要在 script 标签中包裹 type:module
。
还有个很重要的点,两者的 模块化规范 不一样,require 是 CommonJS 规范,这就限制了他只能在 NodeJS 中使用 同步读取模块内容 的方式,所以也就无法在浏览器端用 异步加载脚本文件 的特性
CommonJS 模块的重要特性是加载时执行,即脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。
ES6 模块是动态引用,如果使用 import 从一个模块加载变量,那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
import/export 最终都是编译为 require/exports 来执行的。
CommonJS 规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即 module.exports )是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。
export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。