前端面试 —— JavaScript(一)

数组常用方法有哪些?

    • 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__ ,指向它的原型对象

1.jpg

实例自身属性会屏蔽原型上面的同名属性,实例上没有的属性回去原型上面找

2.jpg

在已经创建了实例的情况下重写原型,会切断现有实例与新原型之间的联系

重写原型对象,会导致原型对象的 constructor 属性指向 Object ,原型链关系混乱。所以我们应该在重写原型对象的时候指定 constructor

People.prototype = {
    constructor: People,
    // ...
}
  • 原型链

JavaScript 中所有的对象都是由它的原型对象继承而来的。而原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链

3.png

所有原型链的终点都是 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 方法不修改原数组,只是返回了一个浅复制原数组中元素的新数组。如果元素是引用类型,只会复制对象的引用(指针);如果是基本数据类型,会把具体的值拷贝过来。

  • 深拷贝实现方式
  1. 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' }}
  1. 函数库 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 } } }
  1. 手撕递归
/**
 * 深拷贝
 * @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]是无效的)

  1. arr.pop() 删除并返回最后一个元素
  2. arr[arr.length - 1] 使用数组的length属性
  3. 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 次

  1. 三元表达式+递归
function multiple(str, n) {
    return n > 1 ? str += multiple(str, --n) : str
}
  1. 数组方法
function multiple2(str, n) {
    return new Array(n + 1).join(str)
}
  1. ES6 字符串方法
function multiple3(str, n) {
    return str.repeat(n)
}

JS 交换变量的方法

  1. 中间变量
  2. ES6 解构
  3. 加减法(针对数字)
  4. 异或运算(针对数字)
  5. 对象和数组
  6. try…catch
  7. 数组的两个值交换

如何用 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
}

具体点分为三步:

  1. 创建一个新对象,继承父类原型上的属性和方法 newObj.__proto__ = oldObj.prototype
  2. 添加父类(People)的属性和方法到新对象中
  3. 如果执行结果有返回值,则返回执行结果;否则返回新创建的对象

代码

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 关键字

  1. 方法中的 this,指向调用方法的对象
  2. 全局环境的 this 指向全局对象
  3. 全局函数其实是 window(全局对象) 的方法
function fun() {
	console.log(this)
}
fun()
// 等价于 window.fun()
  1. 事件中的 this,指向触发事件的 DOM 对象
  2. 构造函数中的 this,指向 new 创建的对象(构造函数是用来创建对象的)
  3. 箭头函数没有 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 的区别?

requireimport
规范CommonJSES6
使用限制(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 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值