跳至主要內容

面试题-JavaScript

望间面试JavaScript大约 19 分钟

面试题-JavaScript

JS 中的数据类型及区别

基本类型(值类型)

在内存中占据固定大小,保存在栈内存中

  • Number(数字)
  • String(字符串)
  • Boolean(布尔)
  • Symbol(符号)
  • null(空)
  • undefined(未定义)

引用类型(复杂数据类型)

保存在堆内存中,栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址

  • Object(对象)
  • Array(数组)
  • Date(日期)
  • RegExp(正则表达式)
  • Function(函数)
  • ...

使用场景

  • Symbol:使用 Symbol 来作为对象属性名(key) 利用该特性,把一些不需要对外操作和访问的属性使用 Symbol 来定义

  • BigInt:由于在 Number 与 BigInt 之间进行转换会损失精度,因而建议仅在值可能大于 253 时使用 BigInt 类型,并且不在两种类型之间进行相互转换

JS 中的数据类型检测方案

typeof

console.log(typeof 1); // number
console.log(typeof true); // boolean
console.log(typeof "mc"); // string
console.log(typeof Symbol); // function
console.log(typeof function () {}); // function
console.log(typeof console.log()); // function
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null); // object
console.log(typeof undefined); // undefined

优点:能够快速区分基本数据类型

缺点:不能将 Object、Array 和 Null 区分,都返回 object

注意

console.log(typeof null == object); // true
console.log(typeof null === object); // false

instanceof

console.log(1 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log("str" instanceof String); // false
console.log([] instanceof Array); // true
console.log(function () {} instanceof Function); // true
console.log({} instanceof Object); // true

优点:能够区分 Array、Object 和 Function,适合用于判断自定义的类实例对象

缺点:Number,Boolean,String 基本数据类型不能判断

Object.prototype.toString.call()

var toString = Object.prototype.toString;
console.log(toString.call(1)); //[object Number]
console.log(toString.call(true)); //[object Boolean]
console.log(toString.call("mc")); //[object String]
console.log(toString.call([])); //[object Array]
console.log(toString.call({})); //[object Object]
console.log(toString.call(function () {})); //[object Function]
console.log(toString.call(undefined)); //[object Undefined]
console.log(toString.call(null)); //[object Null]

优点:精准判断数据类型

缺点:写法繁琐不容易记,推荐进行封装后使用

instanceof 的作用

用于判断一个引用类型是否属于某构造函数;

还可以在继承关系中用来判断一个实例是否属于它的父类型。

instanceof 和 typeof 的区别:

typeof 在对值类型 number、string、boolean 、null 、 undefined、 以及引用类型的 function 的反应是精准的;但是,对于对象{ } 、数组[ ] 、null 都会返回 object

为了弥补这一点,instanceof 从原型的角度,来判断某引用属于哪个构造函数,从而判定它的数据类型。

var & let & const

ES6 之前创建变量用的是 var,之后创建变量用的是 let / const

三者区别

  1. var 定义的变量,没有块的概念,可以跨块访问,不能跨函数访问
    let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问
    const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,且不能修改
  2. var 可以先使用,后声明,因为存在变量提升;let 必须先声明后使用
  3. var 是允许在相同作用域内重复声明同一个变量的,而 letconst 不允许这一现象
  4. 在全局上下文中,基于 let 声明的全局变量和全局对象 GO(window)没有任何关系;
    var 声明的变量会和 GO 有映射关系

字符串常用方法

MDN-Stringopen in new window

数组常用方法

MDN-Arrayopen in new window

常用正则表达式

//(1)匹配 16 进制颜色值
var color = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;

//(2)匹配日期,如 yyyy-mm-dd 格式
var date = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;

//(3)匹配 qq 号
var qq = /^[1-9][0-9]{4,10}$/g;

//(4)手机号码正则
var phone = /^1[34578]\d{9}$/g;

//(5)用户名正则
var username = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;

//(6)Email正则
var email = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;

//(7)身份证号(18位)正则
var cP =
  /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;

//(8)URL正则
var urlP =
  /^((https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;

// (9)ipv4地址正则
var ipP =
  /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;

// (10)//车牌号正则
var cPattern =
  /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/;

// (11)强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间)
var pwd = /^(?=.\d)(?=.[a-z])(?=.[A-Z]).{8,10}$/;

arguments

arguments 对象是函数中传递的参数值的集合

arguments 是一个传递给函数的参数的类数组对象,因为它有一个 length 属性,我们可以使用数组索引表示法 arguments[1]来访问单个值,但它没有数组中的内置方法,如:forEach、reduce、filter、map

注意

箭头函数中没有 arguments 对象

MDN-argumentsopen in new window

解构赋值

解构赋值是 ES6 引入的新特性,数组中的值或对象的属性取出,赋值给其他变量

let a, b, rest;
[a, b] = [10, 20];

console.log(a);
// Expected output: 10

console.log(b);
// Expected output: 20

[a, b, ...rest] = [10, 20, 30, 40, 50];

console.log(rest);
// Expected output: Array [30, 40, 50]
let employee = {
  name: "zs",
  age: 18,
  gender: "male",
};

let { name, age, gender } = employee;

扩展运算符...

如果是字符串、数组就是在语法层面展开;如果是对象,就进行属性拷贝

let str = "hello";
let arr = [1, 2, 3];
let obj = {
  name: "zs",
  age: 18,
  gender: "male",
};

console.log(...str);
// Expected output: h e l l o

console.log(...arr);
// Expected output: 1 2 3

console.log({ ...obj });
// Expected output: { name: "zs", age: 18, gender: "male" }

深浅拷贝

浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝

如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象

实现方法

  • Object.assign() 方法: 用于将所有可枚举属性的值从一个或多个源对象复制到目标对象,返回修改后的对象
  • Array.prototype.slice():返回一个新的数组对象,由 begin 和 end(不包括 end)决定的原数组的浅拷贝
  • 扩展运算符...

深拷贝

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

  • 利用 JSON 的方法

    JSON.parse(JSON.stringify(object));
    
    // 会忽略undefined、symbol、函数,不能处理正则、new Date(),不能解决循环引用
    
  • 封装方法

    function cloneDeep(target,map = new WeakMap()) {
      if(typeOf taret === 'object'){
        let cloneTarget = Array.isArray(target) ? [] : {};
    
        if(map.get(target)) {
            return target;
        }
        map.set(target, cloneTarget);
        for(const key in target){
            cloneTarget[key] = cloneDeep(target[key], map);
        }
        return cloneTarget
      }else{
          return target
      }
    }
    

&&、||、!!

  • &&: 叫逻辑与,在其操作数中找到第一个假值表达式并返回它,如果没有找到任何假值表达式,则返回最后一个真值表达式
  • ||: 叫逻辑或,在其操作数中找到第一个真值表达式并返回它
  • !!: 运算符可以将右侧的值强制转换为布尔值,这也是将值转换为布尔值的一种简单方法

作用域和作用域链

作用域

作用域定义了变量和函数的可见性或可访问性

通俗来说,就是一个变量或函数能不能被访问或引用,是由它的作用域决定的

三种作用域

  • 全局作用域
  • 函数作用域(局部作用域)
  • 块作用域

作用

  • 作用域最大的用处就是隔离变量
  • 不同作用域下同名变量不会有冲突

作用域链

一般情况下,变量到创建该变量的函数的作用域中取值,但是如果在当前作用域中没有查到,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链

闭包

通俗来说,闭包就是能够读取其他函数内部变量的函数

  • 闭包形成的条件

  • 函数的嵌套

  • 内部函数引用外部函数的局部变量

  • 闭包的用途

  • 可以读取函数内部的变量

  • 让这些变量的值始终保持在内存中(防止被回收)

  • 闭包应用场景

  • 发送 ajax 请求成功|失败的回调

  • setTimeout 的延时回调

  • 或者一个函数内部返回另一个匿名函数

JS 中 this 的情况

  1. 普通函数调用:通过 函数名() 直接调用,this指向全局对象window(注意 let 定义的变量不是 window 属性,只有 window.xxx 定义的才是)
  2. 构造函数调用:函数作为构造函数,用 new 关键字调用时:this指向新new出的对象
  3. 对象函数调用:通过对象.函数名() 调用的:this指向这个对象
  4. 箭头函数调用:箭头函数里面没有 this,所以永远是上层作用域this(上下文)
  5. applycall 调用:函数体内 this 的指向的是 call / apply 方法第一个参数,若为空默认是指向全局对象 window
  6. 函数作为 window 内置函数的回调函数调用:this 指向 window(如 setInterval setTimeout 等)

call & apply & bind

call、apply、bind 都是改变 this 指向的方法

call

fn.call(null, 1, 2);

第一个参数为改变的 this 指向谁,后面的参数都是 fn 函数的参数;会立即执行

apply

fn.apply(null, [1, 2]);

第一个参数为改变的 this 指向谁,第二个参数是一个数组,是 fn 函数的参数;会立即执行

bind

fn.bind(null, 1, 2);

call() 类似;不会立即执行

箭头函数的特性

(number) => {
  console.log(number);
};
  1. 箭头函数没有自己的this;会捕获其所在的上下文的 this 值,作为自己的 this 值
  2. 箭头函数没有constructor;是匿名函数,不能作为构造函数,不能通过 new 调用;
  3. 没有new.target 属性;在通过 new 运算符被初始化的函数或构造方法中,new.target 返回一个指向构造方法或函数的引用
  4. 箭头函数不绑定 Arguments 对象
  5. 箭头函数没有原型属性 Fn.prototype 值为 undefined

模板字符串

允许嵌入表达式的字符串,使用反引号 (` `) 来代替普通字符串中的用双引号和单引号

// ES5
let str = "Hello " + name + "!";

// ES6
let str = `Hello ${name}!`;

Set

Set 是唯一值的集合
每个值在 Set 中只能出现一次
一个 Set 可以容纳任何数据类型的任何值

创建实例

let set1 = new Set();
let set2 = new Set(["a", "b", "c", "d", "d", "e"]);

操作

方法或属性说明
size返回元素的个数
add()向 Set 添加新元素
clear()从 Set 中删除所有元素
delete()删除由其值指定的元素
has()如果值存在则返回 true,反之返回 false
forEach()为每个元素调用回调
keys()返回 Set 对象中值的数组
values()与 keys() 相同

Map

Map 以键值对的形式进行数据保存,并且能够记住键的原始插入顺序

Map 中的一个键只能出现一次;它在 Map 的集合中是独一无二的

创建实例

let map = new Map();

操作

方法或属性说明
size返回元素的个数
set()为 Map 对象中的键设置值
get()获取 Map 对象中键的值
keys()返回 Map 对象中键的数组
values()与 keys() 相同

原型 & 原型链

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

原型

在 JS 中,每当定义一个对象(函数也是对象)时,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype  属性,这个属性指向函数的原型对象

原型链

函数的原型对象 constructor 默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型指针__proto__,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似

因此可以利用__proto__一直指向 Object 的原型对象上,而 Object 原型对象用 Object.prototype.__proto__ = null 表示原型链顶端

js 获取原型的方法

  • obj.proto
  • obj.constructor.prototype
  • Object.getPrototypeOf(obj)

new 运算符的实现机制

  1. 首先创建了一个新的空对象
  2. 设置原型,将对象的原型设置为函数的prototype对象
  3. 让函数的this指向这个对象,执行构造函数的代码(为这个新对象添加属性)
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象

DOM & BOM

  • DOM 指的是文档对象模型,它指的是把文档当做一个对象来对待,这个对象主要定义了处理网页内容的方法和接口。
  • BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口;document 对象也是 BOM 的 window 对象的子对象。

DOM 操作

创建新节点

createDocumentFragment(); //创建一个 DOM 片段
createElement(); //创建一个具体的元素
createTextNode(); //创建一个文本节点

添加、移除、替换、插入

appendChild(node); // 添加
removeChild(node); // 移除
replaceChild(new, old); // 替换
insertBefore(new, old); // 插入

查找

getElementById(id); // 通过 id
getElementsByName(name); // 通过 name
getElementsByTagName(tagName); // 通过元素标签
getElementsByClassName(className); // 通过 class
querySelector(selectors); // 获取第一个符合选择器要求的元素
querySelectorAll(selectors); // 获取全部符合选择器要求的元素

属性操作

getAttribute(key); // 获取属性
setAttribute(key, value); // 设置属性
hasAttribute(key); // 判断是否该属性
removeAttribute(key); // 移除该属性

MDN-Documentopen in new window

事件传播

事件传播
事件传播

事件传播有三个阶段

  • 捕获阶段 – 事件从 window 开始,然后向下到每个元素,直到到达目标元素事件或 event.target
  • 目标阶段 – 事件已达到目标元素
  • 冒泡阶段 – 事件从目标元素冒泡,然后上升到每个元素,直到到达 window

addEventListener() 有第三个参数 useCapture,其默认值为 false,事件将在冒泡阶段中发生,如果为 true,则事件将在捕获阶段中发生

addEventListener 详细内容可以看看 MDN 文档 MDN-addEventListeneropen in new window

事件冒泡

冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行

事件捕获

捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行

阻止事件默认行为

e.preventDefault();

事件委托(事件代理)

事件委托指的是把原本需要绑定在子元素的响应事件委托给父元素,让父元素担当事件监听的职务

<ul id="list">
  <li id="item-1">Item 1</li>
  <li id="item-2">Item 2</li>
  <li id="item-3">Item 3</li>
  <li id="item-4">Item 4</li>
  <li id="item-5">Item 5</li>
  <li id="item-6">Item 6</li>
</ul>
// 获取列表元素
document.getElementById("parent-list").addEventListener("click", function (e) {
  // 给这个元素添加一个点击事件
  // 如果点击的这个元素是 li
  if (e.target && e.target.nodeName == "LI") {
    // 获取这个原生的id
    console.log(
      "List item ",
      e.target.id.replace("post-", ""),
      " was clicked!"
    );
  }
});

EventLoop 事件循环

JavaScript 是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,所以会先将同步代码压入执行栈中,依次执行,将异步代码推入异步队列,异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列

执行顺序:同步任务 ==> 微任务 ==> 宏任务

微任务队列的代表:Promise.then、MutationObserver

宏任务的话就是:setImmediate、setTimeout、setInterval

浏览器中的事件环(Event Loop)

事件循环流程
事件循环流程

在主线程执行过程中同步任务会立即执行,遇到异步任务时不会立马执行,会将异步任务放入一个"任务队列"中

当"执行栈"中的所有任务执行完毕,就会去"任务队列"中将对应的任务事件放入执行栈中执行,主线程就是这样重复执行上面的步骤形成一个循环

步骤

  1. 函数入栈,当 Stack 中执行到异步任务的时候,就将他丢给 WebAPIs,接着执行同步任务,直到 Stack 为空;
  2. 此期间 WebAPIs 完成这个事件,把回调函数放入队列中等待执行(微任务放到微任务队列,宏任务放到宏任务队列)
  3. 执行栈为空时,Event Loop 把微任务队列执行清空;
  4. 微任务队列清空后,进入宏任务队列,取队列的第一项任务放入 Stack(栈) 中执行,执行完成后,查看微任务队列是否有任务,有的话,清空微任务队列
  5. 重复,继续从宏任务中取任务执行,执行完成之后,继续清空微任务,如此反复循环,直至清空所有的任务

浏览器中的任务源(task):

  • 宏任务(macrotask)
    宿主环境提供的,比如浏览器
    ajax、setTimeout、setInterval、setTmmediate(只兼容 ie)、script、requestAnimationFrame、messageChannel、UI 渲染、一些浏览器 api
  • 微任务(microtask)
    语言本身提供的,比如 promise.then
    then、queueMicrotask(基于 then)、mutationObserver(浏览器提供)、messageChanne、MutationObserve

Ajax

一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法

//1:创建Ajax对象
var xhr = new XMLHttpRequest();

//2:配置 Ajax 请求地址
xhr.open("get", "index.xml", true);

//3:发送请求
xhr.send(null);

//4:监听请求,接受响应
xhr.onreadysatechange = function () {
  if ((xhr.readySate == 4 && xhr.status == 200) || xhr.status == 304)
    console.log(xhr.responsetXML);
};

setTimeout、Promise、Async / Await 的区别

setTimeout

setTimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行

Promise

Promise 本身是同步的立即执行函数, 当在 executor 中执行 resolve 或者 reject 的时候, 此时是异步操作,会先执行 then / catch 等,当主栈完成后,才会去调用 resolve / reject 中存放的方法执行

console.log("script start");
let promise1 = new Promise(function (resolve) {
  console.log("promise1");
  resolve();
  console.log("promise1 end");
}).then(function () {
  console.log("promise2");
});
setTimeout(function () {
  console.log("settimeout");
});
console.log("script end");
// 输出顺序: script start -> promise 1-> promise1 end -> script end - >promise2 -> settimeout

async / await

async / await 是一个语法糖

async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句

async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}
console.log("script start");
async1();
console.log("script end");
// 输出顺序:script start -> async1 start -> async2 -> script end -> async1 end

Async / Await 如何通过同步的方式实现异步

Async/Await 就是一个自执行的 generate 函数

利用 generate 函数的特性把异步的代码写成“同步”的形式,第一个请求的返回值作为后面一个请求的参数,其中每一个参数都是一个 promise 对象

节流 & 防抖

  • 节流:事件触发后,规定时间内,事件处理函数不能再次被调用
  • 防抖:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时

使用场景

  • 节流:滚动加载更多、搜索框搜的索联想功能、高频点击、表单重复提交……
  • 防抖:搜索框搜索输入,并在输入完以后自动搜索、手机号,邮箱验证输入检测、窗口大小 resize 变化后,再重新渲染

代码

/**
 * 节流函数
 * @param fn要被节流的函数
 * @param delay规定的时间
 */
function throttle(fn, delay) {
  // 记录上一次函数触发的时间
  var lastTime = 0;
  return function () {
    // 记录当前函数触发的时间
    var nowTime = Date.now();
    if (nowTime - lastTime > delay) {
      // 修正this指向问题
      fn.call(this);
      // 同步执行结束时间
      lastTime = nowTime;
    }
  };
}

document.onscroll = throttle(function () {
  console.log("scllor事件被触发了" + Date.now());
}, 200);

/**
 * 防抖函数
 * @param fn要被节流的函数
 * @param delay规定的时间
 */
function debounce(fn, delay) {
  // 记录上一次的延时器
  var timer = null;
  return function () {
    // 清除上一次的演示器
    clearTimeout(timer);
    // 重新设置新的延时器
    timer = setTimeout(function () {
      //修正this指向问题
      fn.apply(this);
    }, delay);
  };
}

document.getElementById("btn").onclick = debounce(function () {
  console.log("按钮被点击了" + Date.now());
}, 1000);

数组扁平化转换

在说到模版编译的时候,有可能会提到数组的转换

[1,2,3,[4,5]]

==>

{
  children:[
    {
      value:1
    },
    {
      value:2
    },
    {
      value:3
    },
    {
      children:[
        {
          value:4
        },
        {
          value:5
        }
      ]
    },
  ]
}
// 测试数组
var arr = [1, 2, 3, [4, 5]];
// 转换函数
function convert(arr) {
  //准备一个接收结果数组
  var result = [];
  // 遍历传入的 arr 的每一项
  for (let i = 0; i < arr.length; i++) {
    //如果遍历到的数字是number,直接放进入
    if (typeof arr[i] == "number") {
      result.push({
        value: arr[i],
      });
    } else if (Array.isArray(arr[i])) {
      //如果遍历到这个项目是数组,那么就递归
      result.push({
        children: convert(arr[i]),
      });
    }
  }
  return { children: result };
}

var o = convert(arr);
console.log(o);

参考

面试题open in new window

由浅入深,66 条 JavaScript 面试知识点open in new window

上次编辑于:
贡献者: ViewRoom