JS面试题整理-1

222 阅读1小时+

说说你对代理的理解

代理有几种定义方式

字面量定义,对象里面的 get和set
类定义, class 中的get和set
Proxy对象,里面传两个对象,第一个对象是目标对象target,第二个对象是专门放get和set的handler对象。Proxy和上面两个的区别在于Proxy专门对对象的属性进行get和set

代理的实际应用有

Vue的双向绑定 vue2用的是Object.defineProperty,vue3用的是proxy
校验值
计算属性值(get的时候加以修饰)

???

根据下面 ES6 构造函数的书写方式,要求写出 ES5 的

出现频率:30 %

掌握难度:60分

作用

参考答案

class Example {

constructor(name) {

this.name = name;

}

init() {

const fun = () => { console.log(this.name) }

fun(); 

}

}

const e = new Example('Hello');

e.init();


function Example(name) {

  'use strict';
  if (!new.target) {
       throw new TypeError('Class constructor cannot be invoked without new');
  }
  this.name = name;

}

Object.defineProperty(Example.prototype, 'init', {

  enumerable: false,
  value: function () {
       'use strict';
       if (new.target) {
           throw new TypeError('init is not a constructor');
       }
       var fun = function () {
           console.log(this.name);
       }
       fun.call(this);
  }

})

解析:

此题的关键在于是否清楚 ES6 的 class 和普通构造函数的区别,记住它们有以下区别,就不会有遗漏:

  1. ES6 中的 class 必须通过 new 来调用,不能当做普通函数调用,否则报错

    因此,在答案中,加入了 new.target 来判断调用方式

  2. ES6 的 class 中的所有代码均处于严格模式之下

因此,在答案中,无论是构造函数本身,还是原型方法,都使用了严格模式

  1. ES6 中的原型方法是不可被枚举的

    因此,在答案中,定义原型方法使用了属性描述符,让其不可枚举

  2. 原型上的方法不允许通过 new 来调用

    因此,在答案中,原型方法中加入了 new.target 来判断调用方式

实际使用场景

数组去重有哪些方法?

出现频率: 70%

掌握难度: 60分

作用

参考答案

// 利用ES6的Set去重,适配范围广,效率一般,书写简单

function unique(arr) {

  return [...new Set(arr)]

}

// 数字或字符串数组去重,效率高

function unique(arr) {

  var result = {}; // 利用对象属性名的唯一性来保证不重复
  for (var i = 0; i < arr.length; i++) {
       if (!result[arr[i]]) {
           result[arr[i]] = true;
       }
  }
  return Object.keys(result); // 获取对象所有属性名的数组

}

// 任意数组去重,适配范围光,效率低

function unique(arr) {

  var result = []; // 结果数组
  for (var i = 0; i < arr.length; i++) {
       if (!result.includes(arr[i])) {
           result.push(arr[i]);
       }
  }
  return result;

}


var arr = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]

// 最low1
let newArr2 = []
for (let i = 0; i < arr.length; i++) {
  if (!newArr2.includes(arr[i])) {
    newArr2.push(arr[i])
  }
}
console.log(newArr2);
// 最low2
let arr2 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]
for (let i = 0; i < arr2.length; i++) {
  var item = arr2[i]
  for (let j = i + 1; j < arr2.length; j++) {
    var compare = arr2[j];
    if (compare === item) {
      arr2.splice(j, 1)
      j--
    }
  }
}
console.log(arr2);


// 基于对象去重
let arr3 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]
let obj = {}
for (let i = 0; i < arr3.length; i++) {

  let item = arr3[i]
  if (obj[item]) {
    arr3[i] = arr3[arr3.length - 1]
    arr3.length--
    i--
    continue;
  }
  obj[item] = item

}
console.log(arr3);
console.log(obj);

// 利用Set
let newArr1 = new Set(arr)
console.log([...newArr1]);


let arr4 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]

//利用reduce
newArr4 = arr4.reduce((prev, curr) => prev.includes(curr)? prev : [...prev,curr],[])
console.log(newArr4);
console.log(document);

实际使用场景

箭头函数有哪些特点

出现频率: 80%

掌握难度:60分

作用

参考答案

  • 更简洁的语法,例如

    • 只有一个形参就不需要用括号括起来
    • 如果函数体只有一行,就不需要放到一个块中
    • 如果 return 语句是函数体内唯一的语句,就不需要 return 关键字
  • 箭头函数没有自己的 thisargumentssuper

  • 箭头函数 this 只会从自己的作用域链的上一层继承 this


  • 更简洁的语法,例如

    • 只有一个形参就不需要用括号括起来
    • 如果函数体只有一行,就不需要放到一个块中
    • 如果 return 语句是函数体内唯一的语句,就不需要 return 关键字
  • 箭头函数没有自己的 thisargumentssuper

  • 箭头函数 this 只会从自己的作用域链的上一层继承 this


  • 外形不同。箭头函数使用箭头定义,普通函数中没有

  • 普通函数可以有匿名函数,也可以有具体名函数,但是箭头函数都是匿名函数。

  • **箭头函数不能用于构造函数,不能使用 new,**普通函数可以用于构造函数,以此创建对象实例。

  • **箭头函数中 this 的指向不同,**在普通函数中,this 总是指向调用它的对象,如果用作构造函数,this 指向创建的对象实例。
    箭头函数本身不创建 this,也可以说箭头函数本身没有 this,但是它在声明时可以捕获其所在上下文的 this 供自己使用。

  • 每一个普通函数调用后都具有一个 arguments 对象,用来存储实际传递的参数。

    但是箭头函数并没有此对象。取而代之用rest参数来解决

  • 箭头函数不能用于 Generator 函数,不能使用 yeild 关键字。

  • 箭头函数不具有 prototype 原型对象。而普通函数具有 prototype 原型对象。

  • 箭头函数不具有 super

  • 箭头函数不具有 new.target


箭头函数主要解决了 this 的指向问题。


箭头函数和普通函数的区别?箭头函数可以当做构造函数 new 吗?

箭头函数是普通函数的简写,但是它不具备很多普通函数的特性 第一点,this指向问题,箭头函数的this指向它定义时所在的对象,而不是调用时所在的对象 不会进行函数提升 没有arguments对象,不能使用arguments,如果要获取参数的话可以使用rest运算符 没有yield属性,不能作为生成器Generator使用 不能new

没有自己的this,不能调用call和apply
没有prototype,new关键字内部需要把新对象的_proto_指向函数的prototype

实际使用场景

说一说类的继承

出现频率:50%

掌握难度:90分

作用

参考答案

  1. 借用构造函数实现继承
function Parent1(){
    this.name = "parent1"
}
function Child1(){
    Parent1.call(this);
    this.type = "child1";
}

缺点:Child1 无法继承 Parent1 的原型对象,并没有真正的实现继承 (部分继承)。

  1. 借用原型链实现继承
function Parent2(){
    this.name = "parent2";
    this.play = [1,2,3];
}
function Child2(){
    this.type = "child2";
}
Child2.prototype = new Parent2();

缺点:原型对象的属性是共享的。

  1. 组合式继承
function Parent3(){
    this.name = "parent3";
    this.play = [1,2,3];
}
function Child3(){
    Parent3.call(this);
    this.type = "child3";
}
Child3.prototype = Object.create(Parent3.prototype);
Child3.prototype.constructor = Child3;

原型继承、组合继承、寄生组合继承、ES6的extend

原型继承

// ----------------------方法一:原型继承
// 原型继承
// 把父类的实例作为子类的原型
// 缺点:子类的实例共享了父类构造函数的引用属性   不能传参

var person = {
  friends: ["a", "b", "c", "d"]
}

var p1 = Object.create(person)

p1.friends.push("aaa")//缺点:子类的实例共享了父类构造函数的引用属性

console.log(p1);
console.log(person);//缺点:子类的实例共享了父类构造函数的引用属性

组合继承

// ----------------------方法二:组合继承
// 在子函数中运行父函数,但是要利用call把this改变一下,
// 再在子函数的prototype里面new Father() ,使Father的原型中的方法也得到继承,最后改变Son的原型中的constructor

// 缺点:调用了两次父类的构造函数,造成了不必要的消耗,父类方法可以复用
// 优点可传参,不共享父类引用属性
function Father(name) {
  this.name = name
  this.hobby = ["篮球", "足球", "乒乓球"]
}

Father.prototype.getName = function () {
  console.log(this.name);
}

function Son(name, age) {
  Father.call(this, name)
  this.age = age
}

Son.prototype = new Father()
Son.prototype.constructor = Son


var s = new Son("ming", 20)

console.log(s);

寄生组合继承

// ----------------------方法三:寄生组合继承
function Father(name) {
  this.name = name
  this.hobby = ["篮球", "足球", "乒乓球"]
}

Father.prototype.getName = function () {
  console.log(this.name);
}

function Son(name, age) {
  Father.call(this, name)
  this.age = age
}

Son.prototype = Object.create(Father.prototype)
Son.prototype.constructor = Son

var s2 = new Son("ming", 18)
console.log(s2);

extend

// ----------------------方法四:ES6的extend(寄生组合继承的语法糖)
//     子类只要继承父类,可以不写 constructor ,一旦写了,则在 constructor 中的第一句话
// 必须是 super 。

class Son3 extends Father { // Son.prototype.__proto__ = Father.prototype
  constructor(y) {
    super(200)  // super(200) => Father.call(this,200)
    this.y = y
  }
}

实际使用场景

new操作符都做了哪些事?

出现频率: 50%

掌握难度: 50分

作用

参考答案

myNew(Fn,arg){

let obj = Object.create(Fn.prototype);
let result = Fn.apply(obj,arg);
return result instanceof Object ? result : obj ;

}

参考答案:

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

new 关键字会进行如下的操作:
步骤 1:创建一个空的简单 JavaScript 对象,即 { } ;
步骤 2:链接该对象到另一个对象(即设置该对象的原型对象);
步骤 3:将步骤 1 新创建的对象作为 this 的上下文;
步骤 4:如果该函数没有返回对象,则返回 this


通过 new 的方式创建对象和通过字面量创建的对象,区别在于 new 出来的对象的原型对象为构造函数.prototype,而字面量对象的原型对象为 Object.prototype

示例代码如下:

function Computer() {}

var c = new Computer(); var d = {}; console.log(c.proto === Computer.prototype); // true console.log(d.proto === Object.prototype); // true

实际使用场景

事件循环机制(宏任务、微任务)

出现频率:80%

掌握难度:70分

作用

参考答案

在 js 中任务会分为同步任务和异步任务。

如果是同步任务,则会在主线程(也就是 js 引擎线程)上进行执行,形成一个执行栈。但是一旦遇到异步任务,则会将这些异步任务交给异步模块去处理,然后主线程继续执行后面的同步代码。

当异步任务有了运行结果以后,就会在任务队列里面放置一个事件,这个任务队列由事件触发线程来进行管理。

一旦执行栈中所有的同步任务执行完毕,就代表着当前的主线程(js 引擎线程)空闲了,系统就会读取任务队列,将可以运行的异步任务添加到执行栈中,开始执行。

在 js 中,任务队列中的任务又可以被分为 2 种类型:宏任务(macrotask)与微任务(microtask

宏任务可以理解为每次执行栈所执行的代码就是一个宏任务,包括每次从事件队列中获取一个事件回调并放到执行栈中所执行的任务。

微任务可以理解为当前宏任务执行结束后立即执行的任务。


  1. 浏览器中的 Event Loop

事件循环中的异步队列有两种:macro(宏任务)队列和 micro(微任务)队列。宏任务队列可以有多个,微任务队列只有一个

  • 常见的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作、UI 渲染等。
  • 常见的 micro-task 比如: process.nextTick、new Promise( ).then(回调)、MutationObserver(html5 新特性) 等。

当某个宏任务执行完后,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务,如果没有,会读取宏任务队列中排在最前的任务,执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,依次类推。

  1. Node 中的事件循环

Node 中的 Event Loop 和浏览器中的是完全不相同的东西。Node.js 采用 V8 作为 js 的解析引擎,而 I/O 处理方面使用了自己设计的 libuvlibuv 是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的 API,事件循环机制也是它里面的实现。

Node.JS 的事件循环分为 6 个阶段:

  • timers 阶段:这个阶段执行 timer( setTimeout、setInterval )的回调
  • I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调
  • idle、prepare 阶段:仅 Node.js 内部使用
  • poll 阶段:获取新的 I/O 事件, 适当的条件下 Node.js 将阻塞在这里
  • check 阶段:执行 setImmediate( )  的回调
  • close callbacks 阶段:执行 socket 的 close 事件回调

Node.js 的运行机制如下:

  • V8 引擎解析 JavaScript 脚本。
  • 解析后的代码,调用 Node API
  • libuv 库负责 Node API 的执行。它将不同的任务分配给不同的线程,形成一个 Event Loop(事件循环),以异步的方式将任务的执行结果返回给 V8 引擎。
  • V8 引擎再将结果返回给用户。

实际使用场景

你了解 node 中的事件循环机制吗?node11 版本以后有什么改变

出现频率:50%

掌握难度:80分

作用

参考答案

Node.js 在主线程里维护了一个事件队列,当接到请求后,就将该请求作为一个事件放入这个队列中,然后继续接收其他请求。当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 I/O 任务,就从线程池中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。

当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop)。

无论是 Linux 平台还是 Windows 平台,Node.js 内部都是通过线程池来完成异步 I/O 操作的,而 LIBUV 针对不同平台的差异性实现了统一调用。因此,Node.js 的单线程仅仅是指 JavaScript 运行在单线程中,而并非 Node.js 是单线程。


单击下面的 html 片段打印的内容是什么?( A

<div onclick="console.log('div')">
  <p onclick="console.log('p')">
    Click here!
  </p>
</div>
  • A: p div
  • B: div p
  • C: p
  • D: div

分析:

onclick 绑定的事件为冒泡型事件。因此当点击 p 标签时,事件会从事件目标开始依次往外触发。

实际使用场景

什么是函数柯里化?

出现频率:30%

掌握难度:50分

作用

参考答案

柯里化(currying)又称部分求值。一个柯里化的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

举个例子,就是把原本:

function(arg1,arg2)  变成 function(arg1)(arg2)
function(arg1,arg2,arg3)  变成 function(arg1)(arg2)(arg3)
function(arg1,arg2,arg3,arg4)  变成 function(arg1)(arg2)(arg3)(arg4)

总而言之,就是将:

function(arg1,arg2,…,argn)  变成 function(arg1)(arg2)…(argn)


函数柯里化原理

function add() {
  var args = Array.prototype.slice.call(arguments)

  var adder = function () {
    args.push(...arguments)
    return adder
  }

  adder.toString = function () {
    return args.reduce((prev, curr) => {
      return prev + curr
    }, 0)
  }

  return adder
}

let a = add(1, 2, 3)
let b = add(1)(2)(3)
console.log(a)
console.log(b)
console.log(add(1, 2)(3));
console.log(Function.toString)

实际使用场景

promise.all 方法的使用场景?数组中必须每一项都是 promise 对象吗?不是 promise 对象会如何处理 ?

出现频率: 40%

掌握难度: 50分

作用

参考答案

*promise.all(promiseArray) * 方法是 promise 对象上的静态方法,该方法的作用是将多个 promise 对象实例包装,生成并返回一个新的 promise 实例。

此方法在集合多个 promise 的返回结果时很有用。

返回值将会按照参数内的 promise 顺序排列,而不是由调用 promise 的完成顺序决定。

promise.all 的特点

接收一个Promise实例的数组或具有Iterator接口的对象

如果元素不是Promise对象,则使用Promise.resolve转成Promise对象

如果全部成功,状态变为resolved,返回值将组成一个数组传给回调

只有有一个失败,状态就变为 rejected,返回值将直接传递给回调 *all( )*的返回值,也是新的 promise 对象


promise.all 方法参数是一个 promise 的数组,只有当所有的 promise 都完成并返回成功,才会调用 resolve,当有一个失败,都会进catch,被捕获错误,promise.all 调用成功返回的结果是每个 promise 单独调用成功之后返回的结果组成的数组,如果调用失败的话,返回的则是第一个 reject 的结果

promise.race 也会调用所有的 promise,返回的结果则是所有 promise 中最先返回的结果,不关心是成功还是失败。


await 表达式会造成异步函数停止执行并且等待promise的解决,当值被resolved,异步函数会恢复执行以及返回resolved值。如果该值不是一个promise,它将会被转换成一个resolved后的promise。如果promiserejectedawait 表达式会抛出异常值。


juejin.cn/post/715564…

juejin.cn/post/715564…

juejin.cn/post/715564…


ajax、axios、fetch 的区别

ajax 是指一种创建交互式网页应用的网页开发技术,并且可以做到无需重新加载整个网页的情况下,能够更新部分网页,也叫作局部更新。

使用 ajax 发送请求是依靠于一个对象,叫 XmlHttpRequest 对象,通过这个对象我们可以从服务器获取到数据,然后再渲染到我们的页面上。现在几乎所有的浏览器都有这个对象,只有 IE7 以下的没有,而是通过 ActiveXObject 这个对象来创建的。

Fetchajax 非常好的一个替代品,基于 Promise 设计,使用 Fetch 来获取数据时,会返回给我们一个 Pormise 对象,但是 Fetch 是一个低层次的 API,想要很好的使用 Fetch,需要做一些封装处理。

下面是 Fetch 的一些缺点

  • Fetch 只对网络请求报错,对 400,500 都当做成功的请求,需要封装去处理
  • Fetch 默认不会带 cookie,需要添加配置项。
  • Fetch 不支持 abort,不支持超时控制,使用 setTimeoutPromise.reject 的实现超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费。
  • Fetch 没有办法原生监测请求的进度,而 XHR 可以。

Vue2.0 之后,axios 开始受到更多的欢迎了。其实 axios 也是对原生 XHR 的一种封装,不过是 Promise 实现版本。它可以用于浏览器和 nodejsHTTP 客户端,符合最新的 ES 规范。

实际使用场景

this 的指向哪几种 ?

出现频率:80%

掌握难度:50分

作用

参考答案

总结起来,this 的指向规律有如下几条:

  • 在函数体中,非显式或隐式地简单调用函数时,在严格模式下,函数内的 this 会被绑定到 undefined 上,在非严格模式下则会被绑定到全局对象 window/global 上。
  • 一般使用 new 方法调用构造函数时,构造函数内的 this 会被绑定到新创建的对象上。
  • 一般通过 call/apply/bind 方法显式调用函数时,函数体内的 this 会被绑定到指定参数的对象上。
  • 一般通过上下文对象调用函数时,函数体内的 this 会被绑定到该对象上。
  • 在箭头函数中,this 的指向是由外层(函数或全局)作用域来决定的。

const shape = {
  radius: 10,
  diameter() {
    return this.radius * 2;
  },
  perimeter: () => 2 * Math.PI * this.radius
};

shape.diameter();
shape.perimeter();
//*20* 和 *NaN*

分析:

diameter 作为对象的方法,其内部的 this 指向调用该方法的对象,因此 this.raduus 获取到的是 shape.radius 的值 10,再乘以 2 输出的值即为 20

perimeter 是一个箭头函数,其内部的 this 应该继承声明时所在上下文中的 this,在这里即继承全局的 this,因此 this.radius 值的为 undefinedundefined 与数值相乘后值为 NaN


function Person(firstName, lastName) {
  this.firstName = firstName;
  this.lastName = lastName;
}

const lydia = new Person("Lydia", "Hallie");
const sarah = Person("Sarah", "Smith");

console.log(lydia);
console.log(sarah);

//*Person { firstName: "Lydia", lastName: "Hallie" }* 和 *undefined*
  • A: Person { firstName: "Lydia", lastName: "Hallie" }undefined

  • B: Person { firstName: "Lydia", lastName: "Hallie" }Person { firstName: "Sarah", lastName: "Smith" }

  • C: Person { firstName: "Lydia", lastName: "Hallie" }{}

  • D: Person { firstName: "Lydia", lastName: "Hallie" }ReferenceError

分析:

lydia 是调用构造函数后得到的实例对象,拥有 firstNamelastName 属性;

sarah 是调用普通函数后得到的返回值,而 Person 作为普通函数没有返回值;

实际使用场景

什么是 js 的闭包?有什么作用?

出现频率: 80%

掌握难度: 40分

作用

参考答案

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

闭包的用处:

  1. 匿名自执行函数
  2. 结果缓存
  3. 封装
  4. 实现类和继承

闭包的应用场景:

  1. 匿名自执行函数
  2. 结果缓存
  3. 封装
  4. 实现类和继承

闭包的缺点:

因为闭包的作用域链会引用包含它的函数的活动对象,导致这些活动对象不会被销毁,因此会占用更多的内存。


什么是作业域?

ES5 中只存在两种作用域:全局作用域和函数作用域。在 JavaScript 中,我们将作用域定义为一套规则,这套规则用来管理引擎如何在当前作用域以及嵌套子作用域中根据标识符名称进行变量(变量名或者函数名)查找。

什么是作用域链?

当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止,,而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。

闭包产生的本质

当前环境中存在指向父级作用域的引用

什么是闭包

闭包是一种特殊的对象,它由两部分组成:执行上下文(代号 A),以及在该执行上下文中创建的函数 (代号 B),当 B 执行时,如果访问了 A 中变量对象的值,那么闭包就会产生,且在 Chrome 中使用这个执行上下文 A 的函数名代指闭包。

一般如何产生闭包

  • 返回函数
  • 函数当做参数传递

闭包的应用场景

  • 柯里化 bind
  • 模块

闭包是指有权访问另外一个函数作用域中的变量的函数。

因为闭包引用着另一个函数的变量,导致另一个函数已经不使用了也无法销毁,所以闭包使用过多,会占用较多的内存,这也是一个副作用,内存泄漏。

如果要销毁一个闭包,可以 把被引用的变量设置为null,即手动清除变量,这样下次 js 垃圾回收机制回收时,就会把设为 null 的量给回收了。

闭包的应用场景:

  1. 匿名自执行函数
  2. 结果缓存
  3. 封装
  4. 实现类和继承

函数执行,形成私有的执行上下文,使内部私有变量不受外界干扰,起到保护和保存的作用

作用:

保护
    避免命名冲突
保存
    解决循环绑定引发的索引问题
变量不会销毁
    可以使用函数内部的变量,使变量不会被垃圾回收机制回收

应用:

设计模式中的单例模式
for循环中的保留i的操作
防抖和节流
函数柯里化

缺点

  • 会出现内存泄漏的问题

实际使用场景

事件委托以及冒泡原理

出现频率:80%

掌握难度:50分

作用

参考答案

事件委托,又被称之为事件代理。在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面整体的运行性能。导致这一问题的原因是多方面的。

首先,每个函数都是对象,都会占用内存。内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。

对事件处理程序过多问题的解决方案就是事件委托。

事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click 事件会一直冒泡到 document 层次。也就是说,我们可以为整个页面指定一个 onclick 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。

事件冒泡(event bubbling),是指事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。


事件委托,又被称之为事件代理。在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面整体的运行性能。导致这一问题的原因是多方面的。

首先,每个函数都是对象,都会占用内存。内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。

对事件处理程序过多问题的解决方案就是事件委托。

事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click 事件会一直冒泡到 document 层次。也就是说,我们可以为整个页面指定一个 onclick 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。

实际使用场景

let const var 的区别?什么是块级作用域?如何用?

出现频率:80%

掌握难度:50分

作用

参考答案

  1. var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问,有变量提升。
  2. let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明。
  3. const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明。

最初在 JS 中作用域有:全局作用域、函数作用域。没有块作用域的概念。

ES6 中新增了块级作用域。块作用域由 { } 包括,if 语句和 for 语句里面的 { } 也属于块作用域。

在以前没有块作用域的时候,在 if 或者 for 循环中声明的变量会泄露成全局变量,其次就是 { } 中的内层变量可能会覆盖外层变量。块级作用域的出现解决了这些问题。

function sayHi() {
  console.log(name);
  console.log(age);
  var name = "Lydia";
  let age = 21;
}

sayHi();  //*undefined* 和 *ReferenceError*

分析:

sayHi 函数内部,通过 var 声明的变量 name 会发生变量提升,var name 会提升到函数作用域的顶部,其默认值为 undefined。因此输出 name 时得到的值为 undefined

let 声明的 age 不会发生变量提升,在输出 age 时该变量还未声明,因此会抛出 ReferenceError 的报错。


var

var声明的变量可进行变量提升,letconst不会
var可以重复声明
var在非函数作用域中定义是挂在到window上的

let

let声明的变量只在局部起作用
let防止变量污染
不可在声明

const

具有let的所有特征
不可被改变
    如果使用const声明的是对象的话,是可以修改对象里面的值的

实际使用场景

JS 的基本数据类型有哪些?基本数据类型和引用数据类型的区别

出现频率:70%

掌握难度:50分

作用

参考答案

在 JavaScript 中,数据类型整体上来讲可以分为两大类:基本类型引用数据类型

基本数据类型,一共有 6 种:

stringsymbolnumberbooleanundefinednull

其中 symbol 类型是在 ES6 里面新添加的基本数据类型。

引用数据类型,就只有 1 种:

object

基本数据类型的值又被称之为原始值或简单值,而引用数据类型的值又被称之为复杂值或引用值。

两者的区别在于:

原始值是表示 JavaScript 中可用的数据或信息的最底层形式或最简单形式。简单类型的值被称为原始值,是因为它们是不可细化的。

也就是说,数字是数字,字符是字符,布尔值是 true 或 falsenull 和 undefined 就是 null 和 undefined。这些值本身很简单,不能够再进行拆分。由于原始值的数据大小是固定的,所以原始值的数据是存储于内存中的栈区里面的。

在 JavaScript 中,对象就是一个引用值。因为对象可以向下拆分,拆分成多个简单值或者复杂值。引用值在内存中的大小是未知的,因为引用值可以包含任何值,而不是一个特定的已知值,所以引用值的数据都是存储于堆区里面。

最后总结一下两者的区别:

  1. 访问方式

    • 原始值:访问到的是值
    • 引用值:访问到的是引用地址
  2. 比较方式

    • 原始值:比较的是值
    • 引用值:比较的是地址
  3. 动态属性

    • 原始值:无法添加动态属性
    • 引用值:可以添加动态属性
  4. 变量赋值

    • 原始值:赋值的是值
    • 引用值:赋值的是地址

    基本类型也称为简单类型,由于其占据空间固定,是简单的数据段,为了便于提升变量查询速度,将其存储在栈中,即按值访问。

引用类型也称为复杂类型,由于其值的大小会改变,所以不能将其存放在栈中,否则会降低变量查询速度,因此,其值存储在堆(heap)中,而存储在变量处的值,是一个指针,指向存储对象的内存处,即按址访问。引用类型除 Object 外,还包括 Function 、Array、RegExp、Date 等等。


关于 JS 基本数据类型有哪些这个问题,可以参阅前面 26 题。 栈和堆的区别在于堆是动态分配内存,内存大小不一,也不会自动释放。栈是自动分配相对固定大小的内存空间,并由系统自动释放。 在 js 中,基本数据都是直接按值存储在栈中的,每种类型的数据占用的内存空间的大小是确定的,并由系统自动分配和自动释放。这样带来的好处就是,内存可以及时得到回收,相对于堆来说,更加容易管理内存空间。 js 中其他类型的数据被称为引用类型的数据(如对象、数组、函数等),它们是通过拷贝和 new 出来的,这样的数据存储于堆中。其实,说存储于堆中,也不太准确,因为,引用类型的数据的地址指针是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据。


juejin.cn/post/715904…


function sum(a, b) {
  return a + b;
}

sum(1, "2");//*"12"*

分析:

任意数据类型在跟 String 做 + 运算时,都会隐式转换为 String 类型。

a 所对应的 Number1,被隐式转换为了 String 值 "1",最终字符串拼接的到 "12"。


const obj = { a: "one", b: "two", a: "three" };
console.log(obj);//{ a: "three", b: "two" }
  • A: { a: "one", b: "two" }
  • B: { b: "two", a: "three" }
  • C: { a: "three", b: "two" }
  • D: SyntaxError

分析:

如果对象有两个具有相同名称的键,则后面的将替前面的键。它仍将处于第一个位置,但具有最后指定的值。


JavaScript 中假值只有 6 个:false""nullundefinedNaN0


juejin.cn/post/715904…


[[0, 1], [2, 3]].reduce(
  (acc, cur) => {
    return acc.concat(cur);
  },
  [1, 2]
);
  • A: [0, 1, 2, 3, 1, 2]
  • B: [6, 1, 2]
  • C: [1, 2, 0, 1, 2, 3]
  • D: [1, 2, 6]

分析:

[1,2] 是我们的初始值。 这是我们开始执行 reduce 函数的初始值,以及第一个 acc 的值。 在第一轮中,acc[1,2]cur[0,1] 。 我们将它们连接起来,结果是 [1,2,0,1]

然后,acc 的值为 [1,2,0,1]cur 的值为 [2,3] 。 我们将它们连接起来,得到 [1,2,0,1,2,3]


[..."Lydia"];//["L", "y", "d", "i", "a"]

分析:

字符串是可迭代的。 扩展运算符将迭代的每个字符映射到一个元素。


强制转换:

  • 转换为 numberparseInt()parseFloat()Number()
  • 转换为 stringString()toString()
  • 转换为 booleanBoolean()

隐式转换:

  • 隐式转换为 number:算术运算/比较运算,例如加、减、乘、除、相等(==)、大于、小于等;
  • 隐式转换为 string:与字符串拼接,例如 + "";
  • 隐式转换为 boolean:逻辑运算,例如或(||)、与(&&)、非(!);

juejin.cn/post/715904…


顺便提一句,栈内存是自动分配内存的。而堆内存是动态分配内存的,不会自动释放。所以每次使用完对象的时候都要把它设置为null,从而减少无用内存的消耗


在JS中为什么0.2+0.1>0.3?

因为在JS中,浮点数是使用64位固定长度来表示的,其中的1位表示符号位,11位用来表示指数位,剩下的52位尾数位,由于只有52位表示尾数位。

而0.1转为二进制是一个无限循环数0.0001100110011001100......(1100循环)

小数的十进制转二进制方法:https://jingyan.baidu.com/article/425e69e6e93ca9be15fc1626.html
要知道,小数的十进制转二进制的方法是和整数不一样的,推荐看一看

由于只能存储52位尾数位,所以会出现精度缺失,把它存到内存中再取出来转换成十进制就不是原来的0.1了,就变成了0.100000000000000005551115123126,而为什么02+0.1是因为

// 0.1 和 0.2 都转化成二进制后再进行运算 0.00011001100110011001100110011001100110011001100110011010 + 0.0011001100110011001100110011001100110011001100110011010 = 0.0100110011001100110011001100110011001100110011001100111

// 转成十进制正好是 0.30000000000000004

那为什么0.2+0.3=0.5呢?⭐⭐⭐⭐

// 0.2 和 0.3 都转化为二进制后再进行计算 0.001100110011001100110011001100110011001100110011001101 + 0.0100110011001100110011001100110011001100110011001101 = 0.10000000000000000000000000000000000000000000000000001 //尾数为大于52位

// 而实际取值只取52位尾数位,就变成了 0.1000000000000000000000000000000000000000000000000000 //0.5

答:0.2 和0.3分别转换为二进制进行计算:在内存中,它们的尾数位都是等于52位的,而他们相加必定大于52位,而他们相加又恰巧前52位尾数都是0,截取后恰好是0.1000000000000000000000000000000000000000000000000000也就是0.5

面试官:那既然0.1不是0.1了,为什么在console.log(0.1)的时候还是0.1呢?⭐⭐⭐

答:在console.log的时候会二进制转换为十进制,十进制再会转为字符串的形式,在转换的过程中发生了取近似值,所以打印出来的是一个近似值的字符串


为什么typeof null是Object⭐⭐⭐⭐

答:

因为在JavaScript中,不同的对象都是使用二进制存储的,如果二进制前三位都是0的话,系统会判断为是Object类型,而null的二进制全是0,自然也就判断为Object

这个bug是初版本的JavaScript中留下的,扩展一下其他五种标识位:

000 对象
1 整型
010 双精度类型
100 字符串
110布尔类型

==是非严格意义上的相等,

两边类型相同,比较大小

两边类型不同,根据下方表格,再进一步进行比较。
    Null == Undefined ->true
    String == Number ->先将String转为Number,在比较大小
    Boolean == Number ->现将Boolean转为Number,在进行比较
    Object == StringNumberSymbol -> Object 转化为原始类型

字面量new出来的对象和 Object.create(null)创建出来的对象有什么区别

  • 字面量和new创建出来的对象会继承Object的方法和属性,他们的隐式原型会指向Object的显式原型,
  • Object.create(null)创建出来的对象原型为null,作为原型链的顶端,自然也没有继承Object的方法和属性

实际使用场景

NaN 是什么的缩写

出现频率:10%

掌握难度: 50分

作用

参考答案

NaN 的全称为 Not a Number,表示非数,或者说不是一个数。虽然 NaN 表示非数,但是它却属于 number 类型。

NaN 有两个特点:

  1. 任何涉及 NaN 的操作都会返回 NaN
  2. NaN 和任何值都不相等,包括它自己本身

String.prototype.giveLydiaPizza = () => {
  return "Just give Lydia pizza already!";
};

const name = "Lydia";

name.giveLydiaPizza();//"Just give Lydia pizza already!"
  • A: "Just give Lydia pizza already!"
  • B: TypeError: not a function
  • C: SyntaxError
  • D: undefined

分析:

String 是一个内置的构造函数,我们可以为它添加属性。 我们给它的原型添加了一个方法。 原始类型的字符串自动转换为字符串对象,由字符串原型函数生成。 因此,所有字符串(字符串对象)都可以访问该方法!

当使用基本类型的字符串调用 giveLydiaPizza 时,实际上发生了下面的过程:

  • 创建一个 String 的包装类型实例
  • 在实例上调用 substring 方法
  • 销毁实例

const a = {};
const b = { key: "b" };
const c = { key: "c" };

a[b] = 123;
a[c] = 456;

console.log(a[b]);//456
  • A: 123
  • B: 456
  • C: undefined
  • D: ReferenceError

分析:

bc 作为一个对象的键时,会自动转换为字符串,而对象自动转换为字符串化时,结果都为 [Object object] 。因此 a[b]a[c] 其实都是同一个属性 a["Object object"]

对象同名的属性后面的值会覆盖前面的,因此最终 a["Object object"] 的值为 456

实际使用场景

JS 的作用域类型

出现频率:50%

掌握难度: 30分

作用

参考答案

在 JavaScript 里面,作用域一共有 4 种:全局作用域,局部作用域、函数作用域以及 eval 作用域。

全局作用域: 这个是默认的代码运行环境,一旦代码被载入,引擎最先进入的就是这个环境。

局部作用域: 当使用 let 或者 const 声明变量时,这些变量在一对花括号中存在局部作用域,只能够在花括号内部进行访问使用。

函数作用域: 当进入到一个函数的时候,就会产生一个函数作用域。函数作用域里面所声明的变量只在函数中提供访问使用。

eval 作用域: 当调用 eval( )  函数的时候,就会产生一个 eval 作用域。

实际使用场景

undefined==null 返回的结果是什么?undefined 与 null 的区别在哪?

出现频率:30%

掌握难度:50分

作用

参考答案

返回 true

这两个值都表示“无”的意思。

通常情况下, 当我们试图访问某个不存在的或者没有赋值的变量时,就会得到一个 undefined 值。Javascript 会自动将声明是没有进行初始化的变量设为 undifined

而 null 值表示空,null 不能通过 Javascript 来自动赋值,也就是说必须要我们自己手动来给某个变量赋值为 null

解析:

那么为什么 JavaScript 要设置两个表示"无"的值呢?这其实是历史原因。

1995 年 JavaScript 诞生时,最初像 Java 一样,只设置了 null 作为表示"无"的值。根据 C 语言的传统,null 被设计成可以自动转为0

但是,JavaScript 的设计者,觉得这样做还不够,主要有以下两个原因。

  1. null 像在 Java 里一样,被当成一个对象。但是,JavaScript 的数据类型分成原始类型(primitive)和合成类型(complex)两大类,作者觉得表示"无"的值最好不是对象。
  2. JavaScript 的最初版本没有包括错误处理机制,发生数据类型不匹配时,往往是自动转换类型或者默默地失败。作者觉得,如果 null 自动转为 0,很不容易发现错误。

因此,作者又设计了一个 undefined

这里注意:先有 null 后有 undefined 出来,undefined 是为了填补之前的坑。

JavaScript 的最初版本是这样区分的:

null 是一个表示"无"的对象(空对象指针),转为数值时为 0

典型用法是:

  • 作为函数的参数,表示该函数的参数不是对象。
  • 作为对象原型链的终点。

undefined 是一个表示"无"的原始值,转为数值时为 NaN

典型用法是:

  • 变量被声明了,但没有赋值时,就等于 undefined
  • 调用函数时,应该提供的参数没有提供,该参数等于 undefined
  • 对象没有赋值的属性,该属性的值为 undefined
  • 函数没有返回值时,默认返回 undefined

实际使用场景

js的异步处理函数

出现频率:10%

掌握难度: 80分

作用

参考答案

在最早期的时候,JavaScript 中要实现异步操作,使用的就是 Callback 回调函数。

但是回调函数会产生回调地狱(Callback Hell

之后 ES6 推出了 Promise 解决方案来解决回调地狱的问题。不过,虽然 Promise 作为 ES6 中提供的一种新的异步编程解决方案,但是它也有问题。比如,代码并没有因为新方法的出现而减少,反而变得更加复杂,同时理解难度也加大。

之后,就出现了基于 Generator 的异步解决方案。不过,这种方式需要编写外部的执行器,而执行器的代码写起来一点也不简单。当然也可以使用一些插件,比如 co 模块来简化执行器的编写。

ES7 提出的 async 函数,终于让 JavaScript 对于异步操作有了终极解决方案。

实际上,async 只是生成器的一种语法糖而已,简化了外部执行器的代码,同时利用 await 替代 yieldasync 替代生成器的*号。

实际使用场景

defer 与 async 的区别

出现频率:10%

掌握难度:50分

作用

参考答案

按照惯例,所有 script 元素都应该放在页面的 head 元素中。这种做法的目的就是把所有外部文件(CSS 文件和 JavaScript 文件)的引用都放在相同的地方。可是,在文档的 head 元素中包含所有 JavaScript 文件,意味着必须等到全部 JavaScript 代码都被下载、解析和执行完成以后,才能开始呈现页面的内容(浏览器在遇到 body 标签时才开始呈现内容)。

对于那些需要很多 JavaScript 代码的页面来说,这无疑会导致浏览器在呈现页面时出现明显的延迟,而延迟期间的浏览器窗口中将是一片空白。为了避免这个问题,现在 Web 应用程序一般都全部 JavaScript 引用放在 body 元素中页面的内容后面。这样一来,在解析包含的 JavaScript 代码之前,页面的内容将完全呈现在浏览器中。而用户也会因为浏览器窗口显示空白页面的时间缩短而感到打开页面的速度加快了。

有了 defer 和 async 后,这种局面得到了改善。

defer (延迟脚本)

延迟脚本:defer 属性只适用于外部脚本文件。

如果给 script 标签定义了defer 属性,这个属性的作用是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,如果 script 元素中设置了 defer 属性,相当于告诉浏览器立即下载,但延迟执行。

async(异步脚本)

异步脚本:async 属性也只适用于外部脚本文件,并告诉浏览器立即下载文件。

但与 defer 不同的是:标记为 async 的脚本并不保证按照指定它们的先后顺序执行。

所以总结起来,两者之间最大的差异就是在于脚本下载完之后何时执行,显然 defer 是最接近我们对于应用脚本加载和执行的要求的。

defer 是立即下载但延迟执行,加载后续文档元素的过程将和脚本的加载并行进行(异步),但是脚本的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。async 是立即下载并执行,加载和渲染后续文档元素的过程将和 js 脚本的加载与执行并行进行(异步)。


众所周知script会阻塞页面的加载,如果我们要是引用外部js,假如这个外部js请求很久的话就难免出现空白页问题,好在官方为我们提供了defer和async

defer

<script src="d.js" defer></script>
<script src="e.js" defer></script>
    1
    2

不会阻止页面解析,并行下载对应的js文件

下载完之后不会执行

等所有其他脚本加载完之后,在DOMContentLoaded事件之前执行对应d.js、e.js

async

<script src="b.js" async></script>
<script src="c.js" async></script>
    1
    2

不会阻止DOM解析,并行下载对应的js文件

下载完之后立即执行

补充,DOMContentLoaded事件

是等HTML文档完全加载完和解析完之后运行的事件
在load事件之前。
不用等样式表、图像等完成加载

实际使用场景

浏览器事件循环和任务队列

出现频率:50%

掌握难度:80分

作用

参考答案

JavaScript 的异步机制由事件循环和任务队列构成。

JavaScript 本身是单线程语言,所谓异步依赖于浏览器或者操作系统等完成。JavaScript 主线程拥有一个执行栈以及一个任务队列,主线程会依次执行代码,当遇到函数时,会先将函数入栈,函数运行完毕后再将该函数出栈,直到所有代码执行完毕。

遇到异步操作(例如:setTimeout、Ajax)时,异步操作会由浏览器(OS)执行,浏览器会在这些任务完成后,将事先定义的回调函数推入主线程的任务队列(task queue)中,当主线程的执行栈清空之后会读取任务队列中的回调函数,当任务队列被读取完毕之后,主线程接着执行,从而进入一个无限的循环,这就是事件循环。

实际使用场景

原型与原型链 (美团 19年)

出现频率:70%

掌握难度:90分

作用

参考答案

参考答案:

  • 每个对象都有一个 __proto__ 属性,该属性指向自己的原型对象
  • 每个构造函数都有一个 prototype 属性,该属性指向实例对象的原型对象
  • 原型对象里的 constructor 指向构造函数本身

每个对象都有自己的原型对象,而原型对象本身,也有自己的原型对象,从而形成了一条原型链条。

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

实际使用场景

作用域与作用域链 (美团 19年)

出现频率:80%

掌握难度:80分

作用

参考答案

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了块级作用域。

作用域链指的是作用域与作用域之间形成的链条。当我们查找一个当前作用域没有定义的变量(自由变量)的时候,就会向上一级作用域寻找,如果上一级也没有,就再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链 。

实际使用场景

同步、异步、宏任微任务执行顺序(描述下列代码的执行结果)

出现频率:60%

掌握难度:80分

作用

参考答案

const first = () => (new Promise((resolve, reject) => {

console.log(3);
let p = new Promise((resolve, reject) => {
    console.log(7);
    setTimeout(() => {
        console.log(1);
    }, 0);
    setTimeout(() => {
        console.log(2);
        resolve(3);
    }, 0)
    resolve(4);
});
resolve(2);
p.then((arg) => {
    console.log(arg, 5); // 1 bb
});
setTimeout(() => {
    console.log(6);
}, 0);

}))

first().then((arg) => {

console.log(arg, 7); // 2 aa
setTimeout(() => {
    console.log(8);
}, 0);

});

setTimeout(() => {

console.log(9);

}, 0);

console.log(10);

参考答案:

3
7
10
4 5
2 7
1
2
6
9
8


同步意味着每一个操作必须等待前一个操作完成后才能执行。 异步意味着操作不需要等待其他操作完成后才开始执行。 在 JavaScript 中,由于单线程的特性导致所有代码都是同步的。但是,有些异步操作(例如:XMLHttpRequest 或 setTimeout)并不是由主线程进行处理的,他们由本机代码(浏览器 API)所控制,并不属于程序的一部分。但程序中被执行的回调部分依旧是同步的。 加分回答:

JavaScript 中的同步任务是指在主线程上排队执行的任务,只有前一个任务执行完成后才能执行后一个任务;异步任务是指进入任务队列(task queue)而非主线程的任务,只有当任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程中进行执行。 JavaScript 的并发模型是基于 “event loop”。 像 alert 这样的方法回阻塞主线程,以致用户关闭他后才能继续进行后续的操作。 JavaScript 主要用于和用户互动及操作 DOM,多线程的情况和异步操作带来的复杂性相比决定了他单线程的特性。 Web Worker 虽然允许 JavaScript 创建多个线程,但子线程完全受主线程控制,且不能操作 DOM。因此他还是保持了单线程的特性。


定时器是属于宏任务(macrotask) 。如果当前执行栈所花费的时间大于定时器时间,那么定时器的回调在宏任务(macrotask) 里,来不及去调用,所有这个时间会有误差。


因为 setTimeout 是异步代码,所以即使后面的时间为 0,也要等到同步代码执行完毕后才会执行。


juejin.cn/post/715564…


juejin.cn/post/715904…


  • 宏任务:scriptsetTimeOutsetIntervalsetImmediate
  • 微任务:promise.then,process.nextTickObject.observeMutationObserver
  • 注意:Promise是同步任务

实际使用场景

如何判断数组或对象(美团 19年)

出现频率:40%

掌握难度:30分

作用

参考答案

  1. 通过 instanceof 进行判断
var arr = [1,2,3,1];
console.log(arr instanceof Array) // true
  1. 通过对象的 constructor 属性
var arr = [1,2,3,1];
console.log(arr.constructor === Array) // true
  1. Object.prototype.toString.call(arr)
console.log(Object.prototype.toString.call({name: "jerry"}));//[object Object]
console.log(Object.prototype.toString.call([]));//[object Array]
  1. 可以通过 ES6 新提供的方法 Array.isArray( )
Array.isArray([]) //true

juejin.cn/post/715904…

实际使用场景

对象深拷贝与浅拷贝,单独问了 Object.assign(美团 19年)

出现频率:60%

掌握难度:50分

作用

参考答案

  • 浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制)

    浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。

  • 深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。

Object.assign 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign 方法进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。


浅拷贝就是只拷贝对象的引用,而不深层次的拷贝对象的值,多个对象指向堆内存中的同一对象,任何一个修改都会使得所有对象的值修改,因为它们共用一条数据。

深拷贝不是单纯的拷贝一份引用数据类型的引用地址,而是将引用类型的值全部拷贝一份,形成一个新的引用类型,这样就不会发生引用错乱的问题,使得我们可以多次使用同样的数据,而不用担心数据之间会起冲突。

解析:

「深拷贝」就是在拷贝数据的时候,将数据的所有引用结构都拷贝一份。简单的说就是,在内存中存在两个数据结构完全相同又相互独立的数据,将引用型类型进行复制,而不是只复制其引用关系。

分析下怎么做「深拷贝」:

  1. 首先假设深拷贝这个方法已经完成,为 deepClone
  2. 要拷贝一个数据,我们肯定要去遍历它的属性,如果这个对象的属性仍是对象,继续使用这个方法,如此往复

function deepClone(o1, o2) {

for (let k in o2) {
    if (typeof o2[k] === 'object') {
        o1[k] = {};
        deepClone(o1[k], o2[k]);
    } else {
        o1[k] = o2[k];
    }
}

}

// 测试用例

let obj = {

a: 1,
b: [1, 2, 3],
c: {}

};

let emptyObj = Object.create(null);

deepClone(emptyObj, obj);

console.log(emptyObj.a == obj.a);

console.log(emptyObj.b == obj.b);

递归容易造成爆栈,尾部调用可以解决递归的这个问题,Chrome 的 V8 引擎做了尾部调用优化,我们在写代码的时候也要注意尾部调用写法。递归的爆栈问题可以通过将递归改写成枚举的方式来解决,就是通过 for 或者 while 来代替递归。


/**

  • 对象克隆

  • 支持基本数据类型及对象

  • 递归方法 */ function clone(obj) {

    var o; switch (typeof obj) {

      case "undefined":
          break;
      case "string":
          o = obj + "";
          break;
      case "number":
          o = obj - 0;
          break;
      case "boolean":
          o = obj;
          break;
      case "object": // object 分为两种情况 对象(Object)或数组(Array)
          if (obj === null) {
              o = null;
          } else {
              if (Object.prototype.toString.call(obj).slice(8, -1) === "Array") {
                  o = [];
                  for (var i = 0; i < obj.length; i++) {
                      o.push(clone(obj[i]));
                  }
              } else {
                  o = {};
                  for (var k in obj) {
                      o[k] = clone(obj[k]);
                  }
              }
          }
          break;
      default:
          o = obj;
          break;
    

    }

    return o;

}


  • 浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制)

    浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。

  • 深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。

浅拷贝方法

  1. 直接赋值
  2. Object.assign 方法:可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。当拷贝的 object 只有一层的时候,是深拷贝,但是当拷贝的对象属性值又是一个引用时,换句话说有多层时,就是一个浅拷贝。
  3. ES6 扩展运算符,当 object 只有一层的时候,也是深拷贝。有多层时是浅拷贝。
  4. Array.prototype.concat 方法
  5. Array.prototype.slice 方法
  6. jQuery 中的  .extend:在 jQuery 中, .extend∗:在∗jQuery∗中,∗.extend(deep,target,object1,objectN)  方法可以进行深浅拷贝。deep 如过设为 true 为深拷贝,默认是 false 浅拷贝。

深拷贝方法

  1. $.extend(deep,target,object1,objectN) ,将 deep 设置为 true
  2. JSON.parse(JSON.stringify) :用 JSON.stringify 将对象转成 JSON 字符串,再用 JSON.parse 方法把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。这种方法虽然可以实现数组或对象深拷贝,但不能处理函数。
  3. 手写递归

示例代码如下:

function deepCopy(oldObj, newobj) {

for (var key in oldObj) {
    var item = oldObj[key];
    // 判断是否是对象
    if (item instanceof Object) {
        if (item instanceof Function) {
            newobj[key] = oldObj[key];
        } else {
            newobj[key] = {};  //定义一个空的对象来接收拷贝的内容
            deepCopy(item, newobj[key]); //递归调用
        }

        // 判断是否是数组
    } else if (item instanceof Array) {
        newobj[key] = [];  //定义一个空的数组来接收拷贝的内容
        deepCopy(item, newobj[key]); //递归调用
    } else {
        newobj[key] = oldObj[key];
    }
}

}


juejin.cn/post/715904…


juejin.cn/post/715904…


// ----------------------------------------------浅拷贝
// 只是把对象的属性和属性值拷贝到另一个对象中
var obj1 = {
  a: {
    a1: { a2: 1 },
    a10: { a11: 123, a111: { a1111: 123123 } }
  },
  b: 123,
  c: "123"
}
// 方式1
function shallowClone1(o) {
  let obj = {}

  for (let i in o) {
    obj[i] = o[i]
  }
  return obj
}

// 方式2
var shallowObj2 = { ...obj1 }

// 方式3
var shallowObj3 = Object.assign({}, obj1)

let shallowObj = shallowClone1(obj1);

shallowObj.a.a1 = 999
shallowObj.b = true

console.log(obj1);  //第一层的没有被改变,一层以下就被改变了



// ----------------------------------------------深拷贝

// 简易版  
function deepClone(o) {
  let obj = {}
  for (var i in o) {
    // if(o.hasOwnProperty(i)){
    if (typeof o[i] === "object") {
      obj[i] = deepClone(o[i])
    } else {
      obj[i] = o[i]
    }
    // }
  }
  return obj
}


var myObj = {
  a: {
    a1: { a2: 1 },
    a10: { a11: 123, a111: { a1111: 123123 } }
  },
  b: 123,
  c: "123"
}

var deepObj1 = deepClone(myObj)
deepObj1.a.a1 = 999
deepObj1.b = false
console.log(myObj);



// 简易版存在的问题:参数没有做检验,传入的可能是 Array、null、regExp、Date
function deepClone2(o) {
  if (Object.prototype.toString.call(o) === "[object Object]") {  //检测是否为对象
    let obj = {}
    for (var i in o) {
      if (o.hasOwnProperty(i)) {
        if (typeof o[i] === "object") {
          obj[i] = deepClone(o[i])
        } else {
          obj[i] = o[i]
        }
      }
    }
    return obj
  } else {
    return o
  }
}

function isObject(o) {
  return Object.prototype.toString.call(o) === "[object Object]" || Object.prototype.toString.call(o) === "[object Array]"
}

// 继续升级,没有考虑到数组,以及ES6中的map、set、weakset、weakmap
function deepClone3(o) {
  if (isObject(o)) {//检测是否为对象或者数组
    let obj = Array.isArray(o) ? [] : {}
    for (let i in o) {
      if (isObject(o[i])) {
        obj[i] = deepClone(o[i])
      } else {
        obj[i] = o[i]
      }
    }
    return obj
  } else {
    return o
  }
}


// 有可能碰到循环引用问题  var a = {}; a.a = a; clone(a);//会造成一个死循环
// 循环检测
// 继续升级
function deepClone4(o, hash = new map()) {
  if (!isObject(o)) return o//检测是否为对象或者数组
  if (hash.has(o)) return hash.get(o)
  let obj = Array.isArray(o) ? [] : {}

  hash.set(o, obj)
  for (let i in o) {
    if (isObject(o[i])) {
      obj[i] = deepClone4(o[i], hash)
    } else {
      obj[i] = o[i]
    }
  }
  return obj
}

// 递归易出现爆栈问题
//  将递归改为循环,就不会出现爆栈问题了
var a1 = { a: 1, b: 2, c: { c1: 3, c2: { c21: 4, c22: 5 } }, d: 'asd' };
var b1 = { b: { c: { d: 1 } } }
function cloneLoop(x) {
  const root = {};
  // 栈 
  const loopList = [  //->[]->[{parent:{a:1,b:2},key:c,data:{ c1: 3, c2: { c21: 4, c22: 5 } }}]
    {
      parent: root,
      key: undefined,
      data: x,
    }
  ];
  while (loopList.length) {
    // 深度优先
    const node = loopList.pop();
    const parent = node.parent; //{} //{a:1,b:2}
    const key = node.key; //undefined //c
    const data = node.data; //{ a: 1, b: 2, c: { c1: 3, c2: { c21: 4, c22: 5 } }, d: 'asd' }  //{ c1: 3, c2: { c21: 4, c22: 5 } }}
    // 初始化赋值目标,key 为 undefined 则拷贝到父元素,否则拷贝到子元素
    let res = parent; //{}->{a:1,b:2,d:'asd'} //{a:1,b:2}->{}
    if (typeof key !== 'undefined') {
      res = parent[key] = {};
    }
    for (let k in data) {
      if (data.hasOwnProperty(k)) {
        if (typeof data[k] === 'object') {
          // 下一次循环 
          loopList.push({
            parent: res,
            key: k,
            data: data[k],
          })
        } else {
          res[k] = data[k];
        }
      }
    }
  }
  return root
}


function deepClone5(o) {
  let result = {}
  let loopList = [    {      parent: result,      key: undefined,      data: o    }  ]

  while (loopList.length) {
    let node = loopList.pop()
    let { parent, key, data } = node
    let anoPar = parent
    if (typeof key !== 'undefined') {
      anoPar = parent[key] = {}
    }

    for (let i in data) {
      if (typeof data[i] === 'object') {
        loopList.push({
          parent: anoPar,
          key: i,
          data: data[i]
        })
      } else {
        anoPar[i] = data[i]
      }
    }
  }
  return result
}


let cloneA1 = deepClone5(a1)
cloneA1.c.c2.c22 = 5555555
console.log(a1);
console.log(cloneA1);


// ------------------------------------------JSON.stringify()实现深拷贝

function cloneJson(o) {
  return JSON.parse(JSON.stringify(o))
}

// let obj = { a: { c: 1 }, b: {} };
// obj.b = obj;
// console.log(JSON.parse(JSON.stringify(obj))) // 报错 // Converting circular structure to JSON

实际使用场景

说说 instanceof 原理

出现频率:30%

掌握难度:70分

作用

参考答案

instanceof 用于检测对象 A 是不是 B 的实例,而检测是基于原型链进行查找的,也就是说 B 的 prototype 有没有在对象 A 的__proto__ 原型链上,如果有就返回 true,否则返回 false

用一段伪代码来模拟其内部执行过程:

instanceof (A,B) = {
    varL = A.__proto__;
    varR = B.prototype;
    if(L === R) {
        // A的内部属性 __proto__ 指向 B 的原型对象
        return true;
    }
    return false;
}

从上述过程可以看出,当 A 的 proto 指向 B 的 prototype 时,就认为 A 就是 B 的实例。

需要注意的是,instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。

例如: [ ] instanceof Object 返回的也会是 true


juejin.cn/post/715904…

实际使用场景

内存泄漏

出现频率: 40%

掌握难度:60分

作用

参考答案

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

Javascript 是一种高级语言,它不像 C 语言那样要手动申请内存,然后手动释放,Javascript 在声明变量的时候自动会分配内存,普通的类型比如 number,一般放在栈内存里,对象放在堆内存里,声明一个变量,就分配一些内存,然后定时进行垃圾回收。垃圾回收的任务由 JavaScript 引擎中的垃圾回收器来完成,它监视所有对象,并删除那些不可访问的对象。

基本的垃圾回收算法称为**“标记-清除”**,定期执行以下“垃圾回收”步骤:

  • 垃圾回收器获取根并**“标记”**(记住)它们。
  • 然后它访问并“标记”所有来自它们的引用。
  • 然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。
  • 以此类推,直到有未访问的引用(可以从根访问)为止。
  • 除标记的对象外,所有对象都被删除。

内存泄露概念 内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。 内存泄漏通常情况下只能由获得程序源代码和程序员才能分析出来。然而,有不少人习惯于把任何不需要的内存使用的增加描述为内存泄漏,即使严格意义上来说这是不准确的。 JS 垃圾收集机制 JS 具有自动回收垃圾的机制,即执行环境会负责管理程序执行中使用的内存。在C和C++等其他语言中,开发者的需要手动跟踪管理内存的使用情况。在编写 JS 代码的时候,开发人员不用再关心内存使用的问题,所需内存的分配 以及无用的回收完全实现了自动管理。 Js中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占的内存,因为只要执行流进入相应的环境,就可能用到它们。而当变量离开环境时,这将其 标记为“离开环境”。 常见内存泄漏以及解决方案

意外的全局变量

Js处理未定义变量的方式比较宽松:未定义的变量会在全局对象创建一个新变量。在浏览器中,全局对象是window。

function foo(arg) {

bar = "this is a hidden global variable"; //等同于window.bar="this is a hidden global variable"
this.bar2= "potential accidental global";//这里的this 指向了全局对象(window),等同于window.bar2="potential accidental global"

}

解决方法:在 JavaScript 程序中添加,开启严格模式'use strict',可以有效地避免上述问题。 注意:那些用来临时存储大量数据的全局变量,确保在处理完这些数据后将其设置为null或重新赋值。与全局变量相关的增加内存消耗的一个主因是缓存。缓存数据是为了重用,缓存必须有一个大小上限才有用。高内存消耗导致缓存突破上限,因为缓 存内容无法被回收。

循环引用

在js的内存管理环境中,对象 A 如果有访问对象 B 的权限,叫做对象 A 引用对象 B。引用计数的策略是将“对象是否不再需要”简化成“对象有没有其他对象引用到它”,如果没有对象引用这个对象,那么这个对象将会被回收 。

let obj1 = { a: 1 }; // 一个对象(称之为 A)被创建,赋值给 obj1,A 的引用个数为 1

let obj2 = obj1; // A 的引用个数变为 2

obj1 = 0; // A 的引用个数变为 1

obj2 = 0; // A 的引用个数变为 0,此时对象 A 就可以被垃圾回收了

但是引用计数有个最大的问题: 循环引用。

function func() {

let obj1 = {};  
let obj2 = {};  

obj1.a = obj2; // obj1 引用 obj2  
obj2.a = obj1; // obj2 引用 obj1  

}

当函数 func 执行结束后,返回值为 undefined,所以整个函数以及内部的变量都应该被回收,但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:

obj1 = null;
obj2 = null;

被遗忘的计时器和回调函数

let someResource = getData();

setInterval(() => {

const node = document.getElementById('Node');  
if(node) {  
    node.innerhtml = JSON.stringify(someResource));  
}  

}, 1000);

上面的例子中,我们每隔一秒就将得到的数据放入到文档节点中去。 但在 setInterval 没有结束前,回调函数里的变量以及回调函数本身都无法被回收。那什么才叫结束呢? 就是调用了 clearInterval。如果回调函数内没有做什么事情,并且也没有被 clear 掉的话,就会造成内存泄漏。 不仅如此,如果回调函数没有被回收,那么回调函数内依赖的变量也没法被回收。上面的例子中,someResource 就没法被回收。同样的,setTiemout 也会有同样的问题。所以,当不需要 interval 或者 timeout 时,最好调用 clearInterval 或者 clearTimeout。

DOM 泄漏

在 JS 中对DOM操作是非常耗时的。因为JavaScript/ECMAScript引擎独立于渲染引擎,而DOM是位于渲染引擎,相互访问需要消耗一定的资源。 而 IE 的 DOM 回收机制便是采用引用计数的,以下主要针对 IE 而言的。 a. 没有清理的 DOM 元素引用

var refA = document.getElementById('refA');

document.body.removeChild(refA);

// refA 不能回收,因为存在变量 refA 对它的引用。将其对 refA 引用释放,但还是无法回收 refA。

解决办法:refA = null;

b. 给 DOM 对象添加的属性是一个对象的引用

var MyObject = {};

document.getElementById('mydiv').myProp = MyObject;

解决方法: 在 window.onunload 事件中写上: document.getElementById('mydiv').myProp = null;

c. DOM 对象与 JS 对象相互引用

function Encapsulator(element) {

this.elementReference = element; 
element.myProp = this; 

}

new Encapsulator(document.getElementById('myDiv'));

解决方法: 在 onunload 事件中写上: document.getElementById('myDiv').myProp = null;

d. 给 DOM 对象用 attachEvent 绑定事件

function doClick() {}

element.attachEvent("onclick", doClick);

解决方法: 在onunload事件中写上: element.detachEvent('onclick', doClick);

e. 从外到内执行 appendChild。这时即使调用 removeChild 也无法释放

var parentDiv = document.createElement("div");

var childDiv = document.createElement("div");

document.body.appendChild(parentDiv);

parentDiv.appendChild(childDiv);

解决方法: 从内到外执行 appendChild:

var parentDiv = document.createElement("div");

var childDiv = document.createElement("div");

parentDiv.appendChild(childDiv);

document.body.appendChild(parentDiv);

JS 的闭包

闭包在 IE6 下会造成内存泄漏,但是现在已经无须考虑了。值得注意的是闭包本身不会造成内存泄漏,但闭包过多很容易导致内存泄漏。闭包会造成对象引用的生命周期脱离当前函数的上下文,如果闭包如果使用不当,可以导致环形引用(circular reference),类似于死锁,只能避免,无法发生之后解决,即使有垃圾回收也还是会内存泄露。

console

控制台日志记录对总体内存配置文件的影响可能是许多开发人员都未想到的极其重大的问题。记录错误的对象可以将大量数据保留在内存中。注意,这也适用于: (1) 在用户键入 JavaScript 时,在控制台中的一个交互式会话期间记录的对象。 (2) 由 console.log 和 console.dir 方法记录的对象。

juejin.cn/post/715564…


内存泄露、垃圾回收机制

面试官:什么是内存泄漏⭐⭐⭐⭐⭐

答:

​ 内存泄露是指不再用的内存没有被及时释放出来,导致该段内存无法被使用就是内存泄漏

面试官:为什么会导致的内存泄漏⭐⭐⭐⭐⭐

答:

内存泄漏指我们无法在通过js访问某个对象,而垃圾回收机制却认为该对象还在被引用,因此垃圾回收机制不会释放该对象,导致该块内存永远无法释放,积少成多,系统会越来越卡以至于崩溃

面试官:垃圾回收机制都有哪些策略?⭐⭐⭐⭐⭐

答:

标记清除法
    垃圾回收机制获取根并标记他们,然后访问并标记所有来自它们的引用,然后在访问这些对象并标记它们的引用…如此递进结束后若发现有没有标记的(不可达的)进行删除,进入执行环境的不能进行删除
引用计数法
    当声明一个变量并给该变量赋值一个引用类型的值时候,该值的计数+1,当该值赋值给另一个变量的时候,该计数+1,当该值被其他值取代的时候,该计数-1,当计数变为0的时候,说明无法访问该值了,垃圾回收机制清除该对象
    缺点: 当两个对象循环引用的时候,引用计数无计可施。如果循环引用多次执行的话,会造成崩溃等问题。所以后来被标记清除法取代。

实际使用场景

ES6 新增哪些东西?让你自己说

出现频率: 50%

掌握难度:70分

作用

参考答案

ES6 新增内容众多,这里列举出一些关键的以及平时常用的新增内容:

  1. 箭头函数
  2. 字符串模板
  3. 支持模块化(import、export
  4. 类(class、constructor、extends
  5. let、const 关键字
  6. 新增一些数组、字符串等内置构造函数方法,例如 Array.fromArray.of 、Math.signMath.trunc 等
  7. 新增一些语法,例如扩展操作符、解构、函数默认参数等
  8. 新增一种基本数据类型 Symbol
  9. 新增元编程相关,例如 proxyReflect
  10. Set 和 Map 数据结构
  11. Promise
  12. Generator 生成器

实际使用场景

weakmap、weakset、map、set

出现频率:40%

掌握难度: 70分

作用

参考答案

WeakSet 对象是一些对象值的集合, 并且其中的每个对象值都只能出现一次。在 WeakSet 的集合中是唯一的

它和 Set 对象的区别有两点:

  • 与 Set 相比,WeakSet 只能是对象的集合,而不能是任何类型的任意值。
  • WeakSet 持弱引用:集合中对象的引用为弱引用。 如果没有其他的对 WeakSet 中对象的引用,那么这些对象会被当成垃圾回收掉。 这也意味着 WeakSet 中没有存储当前对象的列表。 正因为这样,WeakSet 是不可枚举的。

WeakMap 对象也是键值对的集合。它的键必须是对象类型,值可以是任意类型。它的键被弱保持,也就是说,当其键所指对象没有其他地方引用的时候,它会被 GC 回收掉。WeakMap 提供的接口与 Map 相同。

与 Map 对象不同的是,WeakMap 的键是不可枚举的。不提供列出其键的方法。列表是否存在取决于垃圾回收器的状态,是不可预知的。


Set

成员唯一、无序且不重复 键值与键名是一致的(或者说只有键值,没有键名) 可以遍历,方法有 add, delete,has

WeakSet

成员都是对象 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存 DOM 节点,不容易造成内存泄漏 不能遍历,方法有 add, delete,has

Map

本质上是健值对的集合,类似集合 可以遍历,方法很多,可以跟各种数据格式转换

WeakMap

只接受对象作为健名(null 除外),不接受其他类型的值作为健名 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾机制回收,此时键名是无效的 不能遍历,方法有 get、set、has、delete

实际使用场景

防抖和节流?

出现频率:50%

掌握难度:70%

作用

参考答案

我们在平时开发的时候,会有很多场景会频繁触发事件,比如说搜索框实时发请求,onmousemove、resize、onscroll 等,有些时候,我们并不能或者不想频繁触发事件,这时候就应该用到函数防抖和函数节流。

函数防抖(debounce),指的是短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行。

函数节流(throttle),指连续触发事件但是在 n 秒中只执行一次函数。即 2n 秒内执行 2 次... 。节流如字面意思,会稀释函数的执行频率。


我们在平时开发的时候,会有很多场景会频繁触发事件,比如说搜索框实时发请求,onmousemove、resize、onscroll 等,有些时候,我们并不能或者不想频繁触发事件,这时候就应该用到函数防抖和函数节流。

函数防抖(debounce),指的是短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行。

具体实现:

/**

  • 函数防抖

  • @param {function} func 一段时间后,要调用的函数

  • @param {number} wait 等待的时间,单位毫秒 */ function debounce(func, wait){

    // 设置变量,记录 setTimeout 得到的 id let timerId = null; return function(...args){ if(timerId){ // 如果有值,说明目前正在等待中,清除它 clearTimeout(timerId); } // 重新开始计时 timerId = setTimeout(() => { func(...args); }, wait); } }

函数节流(throttle),指连续触发事件但是在 n 秒中只执行一次函数。即 2n 秒内执行 2 次... 。节流如字面意思,会稀释函数的执行频率。

具体实现:

function throttle(func, wait) {

let context, args;
let previous = 0;
return function () {
    let now = +new Date();
    context = this;
    args = arguments;
    if (now - previous > wait) {
        func.apply(context, args);
        previous = now;
    }
}

}


  • 防抖

    • n秒后在执行该事件,若在n秒内被重复触发,则重新计时
  • 节流

    • n秒内只运行一次,若在n秒内重复触发,只有一次生效

实际使用场景

排序算法---(时间复杂度、空间复杂度)

出现频率: 10%

掌握难度:90分

作用

参考答案

算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别。

主要还是从算法所占用的「时间」和「空间」两个维度去考量。

  • 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
  • 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。

因此,评价一个算法的效率主要是看它的时间复杂度和空间复杂度情况。然而,有的时候时间和空间却又是「鱼和熊掌」,不可兼得的,那么我们就需要从中去取一个平衡点。

排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程

排序的分类分为内部排序外部排序法

  • 内部排序:指将需要处理的所有数据都加载到**内部存储器(内存)**中进行排序。
  • 外部排序:数据量过大,无法全部加载到内存中,需要借助**外部存储(文件等)**进行排序。

实际使用场景

async 与 await的作用

出现频率:40%

掌握难度: 70分

作用

参考答案

async 是一个修饰符,async 定义的函数会默认的返回一个 Promise 对象 resolve 的值,因此对 async 函数可以直接进行 then 操作,返回的值即为 then 方法的传入函数。

await 关键字只能放在 async 函数内部, await 关键字的作用就是获取 Promise 中返回的内容, 获取的是 Promise 函数中 resolve 或者 reject 的值。


事件循环中分为宏任务队列和微任务队列。 其中 setTimeout 的回调函数放到宏任务队列里,等到执行栈清空以后执行; promise.then 里的回调函数会放到相应宏任务的微任务队列里,等宏任务里面的同步代码执行完再执行; async 函数表示函数里面可能会有异步方法,await 后面跟一个表达式,async 方法执行时,遇到 await 会立即执行表达式,然后把表达式后面的代码放到微任务队列里,让出执行栈让同步代码先执行。

实际使用场景

typeof null返回结果

出现频率:30%

掌握难度: 10分

作用

参考答案

至于为什么会返回 object,这实际上是来源于 JavaScript 从第一个版本开始时的一个 bug,并且这个 bug 无法被修复。修复会破坏现有的代码。

原理这是这样的,不同的对象在底层都表现为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型,null 的二进制全部为 0,自然前三位也是 0,所以执行 typeof 值会返回 object

实际使用场景

对变量进行类型判断的方式有哪些

出现频率:70%

掌握难度:50分

作用

参考答案

常用的方法有 4 种:

  1. typeof

typeof 是一个操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回的结果用该类型的字符串(全小写字母)形式表示,包括以下 7 种:number、boolean、symbol、string、object、undefined、function 等。

  1. instanceof

instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型。

  1. constructor

当一个函数 F 被定义时,JS 引擎会为 F 添加 prototype 原型,然后再在 prototype 上添加一个 constructor 属性,并让其指向 F 的引用。

  1. toString

toString( )  是 Object 的原型方法,调用该方法,默认返回当前对象的 Class 。这是一个内部属性,其格式为  [object Xxx]  ,其中 Xxx 就是对象的类型。

对于 Object 对象,直接调用 toString( )  就能返回  [object Object]  。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。例如:

Object.prototype.toString.call('') ; // [object String]

Object.prototype.toString.call(1) ; // [object Number]

Object.prototype.toString.call(true) ;// [object Boolean]

Object.prototype.toString.call(Symbol());//[object Symbol]

Object.prototype.toString.call(undefined) ;// [object Undefined]

Object.prototype.toString.call(null) ;// [object Null]

实际使用场景

引用类型有哪些,有什么特点

出现频率:70%

掌握难度:60分

作用

参考答案: JS 中七种内置类型(null,undefined,boolean,number,string,symbol,object)又分为两大类型

两大类型:

  • 基本类型: nullundefinedbooleannumberstringsymbol
  • 引用类型Object: Array ,Function, Date, RegExp

3f177970e2924361902991e8910673cf~tplv-k3u1fbpfcp-zoom-in-crop-mark 1512 0 0 0.webp

基本类型和引用类型的主要区别有以下几点:

存放位置:

  • 基本数据类型:基本类型值在内存中占据固定大小,直接存储在栈内存中的数据
  • 引用数据类型:引用类型在栈中存储了指针,这个指针指向堆内存中的地址,真实的数据存放在堆内存里。

基本类型和引用类型的主要区别有以下几点:

存放位置:

  • 基本数据类型:基本类型值在内存中占据固定大小,直接存储在栈内存中的数据
  • 引用数据类型:引用类型在栈中存储了指针,这个指针指向堆内存中的地址,真实的数据存放在堆内存里。

9cbe609ed3174512b51e643e5d894226~tplv-k3u1fbpfcp-zoom-in-crop-mark 1512 0 0 0.webp

值的可变性:

  • 基本数据类型: 值不可变,javascript 中的原始值(undefined、null、布尔值、数字和字符串)是不可更改的
  • 引用数据类型:引用类型是可以直接改变其值的

比较:

  • 基本数据类型: 基本类型的比较是值的比较,只要它们的值相等就认为他们是相等的
  • 引用数据类型: 引用数据类型的比较是引用的比较,看其的引用是否指向同一个对象

实际使用场景

JS的垃圾回收站机制

出现频率:50%

掌握难度:70分

作用

参考答案

JS 具有自动垃圾回收机制。垃圾收集器会按照固定的时间间隔周期性的执行。

JS 常见的垃圾回收方式:标记清除、引用计数方式。

1、标记清除方式:

  • 工作原理:当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
  • 工作流程:
  • 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记;
  • 去掉环境中的变量以及被环境中的变量引用的变量的标记;
  • 被加上标记的会被视为准备删除的变量;
  • 垃圾回收器完成内存清理工作,销毁那些带标记的值并回收他们所占用的内存空间。

2、引用计数方式:

  • 工作原理:跟踪记录每个值被引用的次数。
  • 工作流程:
  • 声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值的引用次数就是 1
  • 同一个值又被赋值给另一个变量,这个引用类型值的引用次数加1;
  • 当包含这个引用类型值的变量又被赋值成另一个值了,那么这个引用类型值的引用次数减 1
  • 当引用次数变成 0 时,说明没办法访问这个值了;
  • 当垃圾收集器下一次运行时,它就会释放引用次数是0的值所占的内存。

实际使用场景

什么是作用域链、原型链

出现频率: 70%

掌握难度:70分

作用

参考答案

什么是作用域链?

当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止,,而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。

什么原型链?

每个对象都可以有一个原型__proto__,这个原型还可以有它自己的原型,以此类推,形成一个原型链。查找特定属性的时候,我们先去这个对象里去找,如果没有的话就去它的原型对象里面去,如果还是没有的话再去向原型对象的原型对象里去寻找。这个操作被委托在整个原型链上,这个就是我们说的原型链。

实际使用场景

什么是变量提升

出现频率:70%

掌握难度:40分

作用

参考答案

当 JavaScript 编译所有代码时,所有使用 var 的变量声明都被提升到它们的函数/局部作用域的顶部(如果在函数内部声明的话),或者提升到它们的全局作用域的顶部(如果在函数外部声明的话),而不管实际的声明是在哪里进行的。这就是我们所说的“提升”。

请记住,这种“提升”实际上并不发生在你的代码中,而只是一种比喻,与 JavaScript 编译器如何读取你的代码有关。记住当我们想到“提升”的时候,我们可以想象任何被提升的东西都会被移动到顶部,但是实际上你的代码并不会被修改。

函数声明也会被提升,但是被提升到了最顶端,所以将位于所有变量声明之上。

在编译阶段变量和函数声明会被放入内存中,但是你在代码中编写它们的位置会保持不变。

实际使用场景

== 和 === 的区别是什么

出现频率:50%

掌握难度:20分

作用

参考答案

简单来说: == 代表相同, === 代表严格相同(数据类型和值都相等)。

当进行双等号比较时候,先检查两个操作数数据类型,如果相同,则进行===比较,如果不同,则愿意为你进行一次类型转换,转换成相同类型后再进行比较,而 === 比较时,如果类型不同,直接就是false。

从这个过程来看,大家也能发现,某些情况下我们使用 === 进行比较效率要高些,因此,没有歧义的情况下,不会影响结果的情况下,在 JS 中首选 === 进行逻辑比较。


function checkAge(data) {
  if (data === { age: 18 }) {
    console.log("You are an adult!");
  } else if (data == { age: 18 }) {
    console.log("You are still an adult.");
  } else {
    console.log(`Hmm.. You don't have an age I guess`);
  }
}

checkAge({ age: 18 });//*Hmm.. You don't have an age I guess*
  • A: You are an adult!
  • B: You are still an adult.
  • C: Hmm.. You don't have an age I guess

分析:

在比较相等性时,原始类型通过它们的值进行比较,而对象通过它们的引用进行比较。

data 和条件中的 { age: 18 } 两个不同引用的对象,因此永远都不相等。

实际使用场景

Object.is 方法比较的是什么

出现频率:20%

掌握难度:20分

作用

参考答案

Object.is 方法是 ES6 新增的用来比较两个值是否严格相等的方法,与 === (严格相等)的行为基本一致。不过有两处不同:

  • +0 不等于 -0。
  • NaN 等于自身。

所以可以将*Object.is* 方法看作是加强版的严格相等。

实际使用场景

class 是如何实现的

出现频率:60%

掌握难度:80分

作用

参考答案

class 是 ES6 新推出的关键字,它是一个语法糖,本质上就是基于这个原型实现的。只不过在以前 ES5 原型实现的基础上,添加了一些  _classCallCheck、_defineProperties、_createClass等方法来做出了一些特殊的处理。

例如: class Hello {

constructor(x) {

   this.x = x;
   

}

greet() {

   console.log("Hello, " + this.x)
   

}

}

"use strict";

function _classCallCheck(instance, Constructor) {

 if (!(instance instanceof Constructor)) {
     throw new TypeError("Cannot call a class as a function");
 }

}

function _defineProperties(target, props) {

 for (var i = 0; i < props.length; i++) {
     var descriptor = props[i];
     descriptor.enumerable = descriptor.enumerable || false;
     descriptor.configurable = true;
     if ("value" in descriptor)
         descriptor.writable = true;
     Object.defineProperty(target, descriptor.key, descriptor);
 }

}

function _createClass(Constructor, protoProps, staticProps) {

 console.log("Constructor::",Constructor);
 console.log("protoProps::",protoProps);
 console.log("staticProps::",staticProps);
 if (protoProps)
     _defineProperties(Constructor.prototype, protoProps);
 if (staticProps)
     _defineProperties(Constructor, staticProps);
 return Constructor;

}

var Hello = /#PURE/function () {

function Hello(x) { _classCallCheck(this, Hello);

   this.x = x;

}

_createClass(Hello, [{

   key: "greet",
   
   value: function greet() {
   
     console.log("Hello, " + this.x);
     
   }
   
}]);

return Hello;

}();


在 ES6 中,可以书写 class。因为在 ES6 规范中,引入了 class 的概念。使得 JS 开发者终于告别了直接使用原型对象模仿面向对象中的类和类继承时代。

但是 JS 中并没有一个真正的 class 原始类型, class 仅仅只是对原型对象运用语法糖。

之所以出现 class 关键字,是为了使 JS 更像面向对象,所以 ES6 才引入 class 的概念。


class Chameleon {
  static colorChange(newColor) {
    this.newColor = newColor;
  }

  constructor({ newColor = "green" } = {}) {
    this.newColor = newColor;
  }
}

const freddie = new Chameleon({ newColor: "purple" });
freddie.colorChange("orange");//*TypeError*

分析

colorChange 方法是静态的。 静态方法仅在创建它们的构造函数中存在,并且不能传递给任何子级。 由于 freddie 是一个子级对象,函数不会传递,所以在 freddie 实例上不存在 colorChange 方法:抛出TypeError


function Person(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; }

const member = new Person("Lydia", "Hallie"); Person.getFullName = () => this.firstName + this.lastName;

console.log(member.getFullName());//TypeError

  • A: TypeError
  • B: SyntaxError
  • C: Lydia Hallie
  • D: undefined undefined

Person.getFullName 是将方法添加到了函数身上,因此当我们通过实例对象 member 去调用该方法时并不能找到该方法。

实际使用场景

ES6 中模块化导入和导出与 common.js 有什么区别

出现频率:20%

掌握难度:30分

作用

参考答案: CommonJs模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化不会影响到这个值. // common.js var count = 1;

var printCount = () =>{

return ++count;

}

module.exports = {

printCount: printCount,
count: count

};

// index.js

let v = require('./common');

console.log(v.count); // 1

console.log(v.printCount()); // 2

console.log(v.count); // 1

你可以看到明明common.js里面改变了count,但是输出的结果还是原来的。这是因为count是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动的值。将common.js里面的module.exports 改写成

module.exports = {

printCount: printCount,
get count(){
    return count
}

};

这样子的输出结果是 1,2,2

而在ES6当中,写法是这样的,是利用export 和import导入的

// es6.js

export let count = 1;

export function printCount() {

++count;

}

// main1.js

import { count, printCount } from './es6';

console.log(count)

console.log(printCount());

console.log(count)

ES6 模块是动态引用,并且不会缓存,模块里面的变量绑定其所有的模块,而是动态地去加载值,并且不能重新赋值,

ES6 输入的模块变量,只是一个“符号连接符”,所以这个变量是只读的,对它进行重新赋值会报错。如果是引用类型,变量指向的地址是只读的,但是可以为其添加属性或成员。

另外还想说一个 export default

let count = 1;

function printCount() {

++count;

} export default { count, printCount}

// main3.js

import res form './main3.js'

console.log(res.count)

export与export default的区别及联系:

  1. export与export default均可用于导出常量、函数、文件、模块等
  2. 你可以在其它文件或模块中通过 import + (常量 | 函数 | 文件 | 模块)名的方式,将其导入,以便能够对其进行使用
  3. 在一个文件或模块中,export、import可以有多个,export default仅有一个
  4. 通过export方式导出,在导入时要加{ },export default则不需要。

——————————————————————————————————————

juejin.cn/post/715904…


为什么要使用模块化

  • 防止命名冲突
  • 更好的分离,按需加载
  • 更好的复用性
  • 更高的维护性

面试官:exports和module.exports有什么区别?⭐⭐⭐

导出方式不一样
    exports.xxx='xxx'
    module.export = {}
exportsmodule.exports的引用,两个指向的是用一个地址,而require能看到的只有module.exports

面试官:JS模块包装格式有哪些?⭐⭐⭐

commonjs
    同步运行,不适合前端

AMD
    异步运行
    异步模块定义,主要采用异步的方式加载模块,模块的加载不影响后面代码的执行。所有依赖这个模块的语句都写在一个回调函数中,模块加载完毕,再执行回调函数

CMD
    异步运行
    seajs 规范

面试官:ES6和commonjs的区别⭐⭐⭐

  • commonjs模块输出的是值的拷贝,而ES6输出的值是值的引用
  • commonjs是在运行时加载,是一个对象,ES6是在编译时加载,是一个代码块
  • commonjs的this指向当前模块,ES6的this指向undefined

实际使用场景

了解过 js 中 arguments 吗?接收的是实参还是形参?

出现频率: 10%

掌握难度:10分

作用

参考答案

JS 中的 arguments 是一个伪数组对象。这个伪数组对象将包含调用函数时传递的所有的实参。

与之相对的,JS 中的函数还有一个 length 属性,返回的是函数形参的个数。

实际使用场景

强制类型转换方法有哪些?

出现频率: 50%

掌握难度:70分

作用

参考答案

JavaScript 中的数据类型转换,主要有三种方式:

  1. 转换函数

js 提供了诸如 parseInt 和 parseFloat 这些转换函数,通过这些转换函数可以进行数据类型的转换 。

  1. 强制类型转换

还可使用强制类型转换(type casting)处理转换值的类型。

例如:

  • Boolean(value) 把给定的值转换成 Boolean 型;
  • Number(value)——把给定的值转换成数字(可以是整数或浮点数);
  • String(value)——把给定的值转换成字符串。
  1. 利用 js 变量弱类型转换。

例如:

  • 转换字符串:直接和一个空字符串拼接,例如:a = "" + 数据
  • 转换布尔:!!数据类型,例如:!!"Hello"
  • 转换数值:数据*1 或 /1,例如:"Hello * 1"

类型转换可以分为两种,隐性转换显性转换

1. 隐性转换

当不同数据类型之间进行相互运算,或者当对非布尔类型的数据求布尔值的时候,会发生隐性转换。

预期为数字的时候:算术运算的时候,我们的结果和运算的数都是数字,数据会转换为数字来进行计算。

类型转换前转换后
number44
string"1"1
string"abc"NaN
string""0
booleantrue1
booleanfalse0
undefinedundefinedNaN
nullnull0

预期为字符串的时候:如果有一个操作数为字符串时,使用+符号做相加运算时,会自动转换为字符串。

预期为布尔的时候:前面在介绍布尔类型时所提到的 9 个值会转为 false,其余转为 true

2. 显性转换

所谓显性转换,就是只程序员强制将一种类型转换为另外一种类型。显性转换往往会使用到一些转换方法。常见的转换方法如下:

  • 转换为数值类型:Number()parseInt()parseFloat()
  • 转换为布尔类型:Boolean()
  • 转换为字符串类型:toString()String()

当然,除了使用上面的转换方法,我们也可以通过一些快捷方式来进行数据类型的显性转换,如下:

  • 转换字符串:直接和一个空字符串拼接,例如:a = "" + 数据
  • 转换布尔:!!数据类型,例如:!!"Hello"
  • 转换数值:数据*1 或 /1,例如:"Hello * 1"

实际使用场景

纯函数

出现频率:30%

掌握难度:20分

作用

参考答案

一个函数,如果符合以下两个特点,那么它就可以称之为纯函数:

对于相同的输入,永远得到相同的输出
没有任何可观察到的副作用

解析:

针对上面的两个特点,我们一个一个来看。

相同输入得到相同输出

我们先来看一个不纯的反面典型:

let greeting = 'Hello'

function greet (name) {

return greeting + ' ' + name

}

console.log(greet('World')) // Hello World

上面的代码中,greet('World') 是不是永远返回 Hello World ? 显然不是,假如我们修改 greeting 的值,就会影响 greet 函数的输出。即函数 greet 其实是 依赖外部状态 的。

那我们做以下修改:

function greet (greeting, name) {

return greeting + ' ' + name }

console.log(greet('Hi', 'Savo')) // Hi Savo

将 greeting 参数也传入,这样对于任何输入参数,都有与之对应的唯一的输出参数了,该函数就符合了第一个特点。

没有副作用

副作用的意思是,这个函数的运行,不会修改外部的状态。

下面再看反面典型:

const user = { username: 'savokiss' }

let isValid = false

function validate (user) {

if (user.username.length > 4) {

isValid = true

} }

可见,执行函数的时候会修改到 isValid 的值(注意:如果你的函数没有任何返回值,那么它很可能就具有副作用!)

那么我们如何移除这个副作用呢?其实不需要修改外部的 isValid 变量,我们只需要在函数中将验证的结果 return 出来:

const user = {

username: 'savokiss' }

function validate (user) {

return user.username.length > 4; }

const isValid = validate(user)

这样 validate 函数就不会修改任何外部的状态了~

实际使用场景

JS 模块化

出现频率:20%

掌握难度:50分

作用

参考答案

模块化主要是用来抽离公共代码,隔离作用域,避免变量冲突等。

模块化的整个发展历史如下:

IIFE: 使用自执行函数来编写模块化,特点:在一个单独的函数作用域中执行代码,避免变量冲突。

(function(){

return {

data:[]
    

}

})()

AMD: 使用requireJS 来编写模块化,特点:依赖必须提前声明好。

define('./index.js',function(code){

// code 就是index.js 返回的内容

})

CMD: 使用seaJS 来编写模块化,特点:支持动态引入依赖文件。

define(function(require, exports, module) {

var indexCode = require('./index.js'); });

CommonJS: nodejs 中自带的模块化。

var fs = require('fs');

UMD:兼容AMD,CommonJS 模块化语法。

webpack(require.ensure) :webpack 2.x 版本中的代码分割。

ES Modules: ES6 引入的模块化,支持import 来引入另一个 js 。

import a from 'a';


juejin.cn/post/715904…

实际使用场景

apply call bind 区别,手写

出现频率:40%

掌握难度:80分

作用

参考答案

juejin.cn/post/715359…


call和apply实现思路主要是:
    判断是否是函数调用,若非函数调用抛异常
    通过新对象(context)来调用函数
        给context创建一个fn设置为需要调用的函数
        结束调用完之后删除fn
bind实现思路
    判断是否是函数调用,若非函数调用抛异常
    返回函数
        判断函数的调用方式,是否是被new出来的
            new出来的话返回空对象,但是实例的__proto__指向_this的prototype
    完成函数柯里化
        Array.prototype.slice.call()

call:

Function.prototype.myCall = function (context) {
  // 先判断调用myCall是不是一个函数
  // 这里的this就是调用myCall的
  if (typeof this !== 'function') {
    throw new TypeError("Not a Function")
  }

  // 不传参数默认为window
  context = context || window

  // 保存this
  context.fn = this

  // 保存参数
  let args = Array.from(arguments).slice(1)   //Array.from 把伪数组对象转为数组

  // 调用函数
  let result = context.fn(...args)

  delete context.fn

  return result

}

apply:

Function.prototype.myApply = function (context) {

      // 判断this是不是函数
      if (typeof this !== "function") {
        throw new TypeError("Not a Function")
      }

      let result

      // 默认是window
      context = context || window

      // 保存this
      context.fn = this

      // 是否传参
      if (arguments[1]) {
        result = context.fn(...arguments[1])
      } else {
        result = context.fn()
      }
      delete context.fn

      return result
    }

bind

Function.prototype.myBind = function(context){
  // 判断是否是一个函数
  if(typeof this !== "function") {
    throw new TypeError("Not a Function")
  }
  // 保存调用bind的函数
  const _this = this 
  // 保存参数
  const args = Array.prototype.slice.call(arguments,1)
  // 返回一个函数
  return function F () {
    // 判断是不是new出来的
    if(this instanceof F) {
      // 如果是new出来的
      // 返回一个空对象,且使创建出来的实例的__proto__指向_this的prototype,且完成函数柯里化
      return new _this(...args,...arguments)
    }else{
      // 如果不是new出来的改变this指向,且完成函数柯里化
      return _this.apply(context,args.concat(...arguments))
    }
  } 
}

实际使用场景

手写 reduce flat

出现频率:40%

掌握难度:80分

作用

参考答案

juejin.cn/post/715359…

实际使用场景

== 隐试转换的原理?是怎么转换的

出现频率:60%

掌握难度:80分

作用

参考答案

两个与类型转换有关的函数:valueOf()和toString()

  • valueOf()的语义是,返回这个对象逻辑上对应的原始类型的值。比如说,String包装对象的valueOf(),应该返回这个对象所包装的字符串。
  • toString()的语义是,返回这个对象的字符串表示。用一个字符串来描述这个对象的内容。

valueOf()和toString()是定义在Object.prototype上的方法,也就是说,所有的对象都会继承到这两个方法。但是在Object.prototype上定义的这两个方法往往不能满足我们的需求(Object.prototype.valueOf()仅仅返回对象本身),因此js的许多内置对象都重写了这两个函数,以实现更适合自身的功能需要(比如说,String.prototype.valueOf就覆盖了在Object.prototype中定义的valueOf)。当我们自定义对象的时候,最好也重写这个方法。重写这个方法时要遵循上面所说的语义。

js内部用于实现类型转换的4个函数

这4个方法实际上是ECMAScript定义的4个抽象的操作,它们在js内部使用,进行类型转换。js的使用者不能直接调用这些函数。

  • ToPrimitive ( input [ , PreferredType ] )
  • ToBoolean ( argument )
  • ToNumber ( argument )
  • ToString ( argument )

需要区分这里的 ToString() 和上文谈到的 toString(),一个是 js 引擎内部使用的函数,另一个是定义在对象上的函数。

(1)ToPrimitive ( input [ , PreferredType ] )

将 input 转化成一个原始类型的值。PreferredType参数要么不传入,要么是Number 或 String。如果PreferredType参数是Number,ToPrimitive这样执行:

  1. 如果input本身就是原始类型,直接返回input。
  2. 调用input.valueOf() ,如果结果是原始类型,则返回这个结果。
  3. 调用input.toString() ,如果结果是原始类型,则返回这个结果。
  4. 抛出TypeError异常。

以下是PreferredType不为Number时的执行顺序。

  • 如果PreferredType参数是String,则交换上面这个过程的第2和第3步的顺序,其他执行过程相同。

  • 如果PreferredType参数没有传入

    • 如果input是内置的Date类型,PreferredType 视为String
    • 否则PreferredType 视为 Number

可以看出,ToPrimitive依赖于valueOf和toString的实现。

(2)ToBoolean ( argument )

两个与类型转换有关的函数:valueOf()和toString()

  • valueOf()的语义是,返回这个对象逻辑上对应的原始类型的值。比如说,String包装对象的valueOf(),应该返回这个对象所包装的字符串。
  • toString()的语义是,返回这个对象的字符串表示。用一个字符串来描述这个对象的内容。

valueOf()和toString()是定义在Object.prototype上的方法,也就是说,所有的对象都会继承到这两个方法。但是在Object.prototype上定义的这两个方法往往不能满足我们的需求(Object.prototype.valueOf()仅仅返回对象本身),因此js的许多内置对象都重写了这两个函数,以实现更适合自身的功能需要(比如说,String.prototype.valueOf就覆盖了在Object.prototype中定义的valueOf)。当我们自定义对象的时候,最好也重写这个方法。重写这个方法时要遵循上面所说的语义。

js内部用于实现类型转换的4个函数

这4个方法实际上是ECMAScript定义的4个抽象的操作,它们在js内部使用,进行类型转换。js的使用者不能直接调用这些函数。

  • ToPrimitive ( input [ , PreferredType ] )
  • ToBoolean ( argument )
  • ToNumber ( argument )
  • ToString ( argument )

需要区分这里的 ToString() 和上文谈到的 toString(),一个是 js 引擎内部使用的函数,另一个是定义在对象上的函数。

(1)ToPrimitive ( input [ , PreferredType ] )

将 input 转化成一个原始类型的值。PreferredType参数要么不传入,要么是Number 或 String。如果PreferredType参数是Number,ToPrimitive这样执行:

  1. 如果input本身就是原始类型,直接返回input。
  2. 调用input.valueOf() ,如果结果是原始类型,则返回这个结果。
  3. 调用input.toString() ,如果结果是原始类型,则返回这个结果。
  4. 抛出TypeError异常。

以下是PreferredType不为Number时的执行顺序。

  • 如果PreferredType参数是String,则交换上面这个过程的第2和第3步的顺序,其他执行过程相同。

  • 如果PreferredType参数没有传入

    • 如果input是内置的Date类型,PreferredType 视为String
    • 否则PreferredType 视为 Number

可以看出,ToPrimitive依赖于valueOf和toString的实现。

(2)ToBoolean ( argument )

9fa45b887099455781de45fb9a76f327~tplv-k3u1fbpfcp-zoom-in-crop-mark 1512 0 0 0.webp

只需要记忆 0, null, undefined, NaN, ""  返回 false 就可以了,其他一律返回 true

(3)ToNumber ( argument )

9ad3882dfe664ca99d6aa14b4e28ad55~tplv-k3u1fbpfcp-zoom-in-crop-mark 1512 0 0 0.webp

ToNumber的转化并不总是成功,有时会转化成NaN,有时则直接抛出异常。

(4)ToString ( argument )

4627620de516406ea6273b0ccc8c964b~tplv-k3u1fbpfcp-zoom-in-crop-mark 1512 0 0 0.webp

当js期望得到某种类型的值,而实际在那里的值是其他的类型,就会发生隐式类型转换。系统内部会自动调用我们前面说ToBoolean ( argument )、ToNumber ( argument )、ToString ( argument ),尝试转换成期望的数据类型。

实际使用场景

实现 5.add(3).sub(2) (百度)

出现频率:10%

掌握难度:30分

作用

参考答案

这里想要实现的是链式操作,那么我们可以考虑在 Number 类型的原型上添加 add 和 sub 方法,这两个方法返回新的数

示例如下:

Number.prototype.add = function (number) {

if (typeof number !== 'number') {
    throw new Error('请输入数字~');
}
return this.valueOf() + number;

};

Number.prototype.minus = function (number) {

if (typeof number !== 'number') {
    throw new Error('请输入数字~');
}
return this.valueOf() - number;

};

console.log((5).add(3).minus(2)); // 6


juejin.cn/post/715904…

实际使用场景

为什么普通 for 循环的性能远远高于 forEach 的性能,请解释其中的原因。

出现频率: 20%

掌握难度:40分

作用

参考答案

for 循环按顺序遍历,forEach 使用 iterator 迭代器遍历

下面是一段性能测试的代码:

let arrs = new Array(100000);

console.time('for');

for (let i = 0; i < arrs.length; i++) {

};

console.timeEnd('for');

console.time('forEach');

arrs.forEach((arr) => {

}); console.timeEnd('forEach');

for: 2.263ms

forEach: 0.254ms

在10万这个级别下,forEach的性能是for的十倍

for: 2.263ms

forEach: 0.254ms

在100万这个量级下,forEach的性能是和for的一致

for: 2.844ms

forEach: 2.652ms

在1000万级以上的量级上 ,forEach的性能远远低于for的性能

for: 8.422ms

forEach: 30.328m

我们从语法上面来观察:

arr.forEach(callback(currentValue [, index [, array]])[, thisArg])

可以看到 forEach 是有回调的,它会按升序为数组中含有效值的每一项执行一次 callback,且除了抛出异常以外,也没有办法中止或者跳出 forEach 循环。那这样的话执行就会额外的调用栈和函数内的上下文。 而 for 循环则是底层写法,不会产生额外的消耗。 在实际业务中没有很大的数组时,for 和 forEach 的性能差距其实很小,forEach 甚至会优于 for 的时间,且更加简洁,可读性也更高,一般也会优先使用 forEach 方法来进行数组的循环处理。


JavaScript 原有的 for...in 循环,只能获得对象的键名,不能直接获取键值。ES6 提供 for...of 循环,允许遍历获得键值。

例如:

var arr = ['a', 'b', 'c', 'd'];

for (let a in arr) {

console.log(a); // 0 1 2 3

}

for (let a of arr) {

console.log(a); // a b c d

}

实际使用场景

数组里面有 10 万个数据,取第一个元素和第 10 万个元素的时间相差多少(字节)

出现频率: 10%

掌握难度:20分

作用

参考答案

消耗时间几乎一致,差异可以忽略不计

解析:

数组可以直接根据索引取的对应的元素,所以不管取哪个位置的元素的时间复杂度都是 O(1) JavaScript 没有真正意义上的数组,所有的数组其实是对象,其“索引”看起来是数字,其实会被转换成字符串,作为属性名(对象的 key)来使用。所以无论是取第 1 个还是取第 10 万个元素,都是用 key 精确查找哈希表的过程,其消耗时间大致相同。

实际使用场景

怎么添加、移除、复制、创建、和查找节点

出现频率: 20%

掌握难度:30分

作用

参考答案

1)创建新节点 createDocumentFragment( ) // 创建一个DOM 片段 createElement( ) // 创建一个具体的元素 createTextNode( ) // 创建一个文本节点 (2)添加、移除、替换、插入 appendChild( ) removeChild( ) replaceChild( ) insertBefore( ) // 在已有的子节点前插入一个新的子节点 (3)查找 getElementsByTagName( ) //通过标签名称 getElementsByName( ) // 通过元素的 Name 属性的值 getElementById( ) // 通过元素 Id,唯一性 querySelector( ) // 用于接收一个 CSS 选择符,返回与该模式匹配的第一个元素 querySelectorAll( ) // 用于选择匹配到的所有元素

实际使用场景

console.log(1+'2')和 console.log(1-'2')的打印结果

出现频率: 10%

掌握难度:20分

作用

参考答案

第一个打印出 '12',是一个 string 类型的值。

第二个打印出 -1,是一个 number 类型的值

实际使用场景

JS 延迟加载的方式有哪些?

出现频率: 30%

掌握难度:30分

作用

参考答案

  • defer 属性
  • async 属性
  • 使用 jQuery 的 getScript( ) 方法
  • 使用 setTimeout 延迟方法
  • 把 JS 外部引入的文件放到页面底部,来让 JS 最后引入

实际使用场景

说说严格模式的限制

出现频率: 30%

掌握难度:50分

作用

参考答案

什么是严格模式?

严格模式对 JavaScript 的语法和行为都做了一些更改,消除了语言中一些不合理、不确定、不安全之处;提供高效严谨的差错机制,保证代码安全运行;禁用在未来版本中可能使用的语法,为新版本做好铺垫。在脚本文件第一行或函数内第一行中引入"use strict"这条指令,就能触发严格模式,这是一条没有副作用的指令,老版的浏览器会将其作为一行字符串直接忽略。

例如:

进入严格模式后的限制

  • 变量必须声明后再赋值
  • 不能有重复的参数名,函数的参数也不能有同名属性
  • 不能使用with语句
  • 不能对只读属性赋值
  • 不能使用前缀 0表示八进制数
  • 不能删除不可删除的属性
  • eval 不会在它的外层作用域引入变量。
  • evalarguments不能被重新赋值
  • arguments 不会自动反应函数的变化
  • 不能使用 arguments.callee
  • 不能使用 arguments.caller
  • 禁止 this 指向全局对象
  • 不能使用 fn.caller 和 fn.arguments 获取函数调用的堆栈
  • 增加了保留字

实际使用场景

attribute和 property 的区别是什么?

出现频率: 10%

掌握难度:10分

作用

参考答案

property 和 attribute 非常容易混淆,两个单词的中文翻译也都非常相近(property:属性,attribute:特性),但实际上,二者是不同的东西,属于不同的范畴。

  • property是DOM中的属性,是JavaScript里的对象;
  • attribute是HTML标签上的特性,它的值只能够是字符串;

简单理解,Attribute就是dom节点自带的属性,例如html中常用的id、class、title、align等。

而Property是这个DOM元素作为对象,其附加的内容,例如childNodes、firstChild等。

实际使用场景

常见兼容性问题

出现频率: 70%

掌握难度:80分

作用

参考答案

常见的兼容性问题很多,这里列举一些:

  1. 关于获取行外样式 currentStyle 和 getComputedStyle 出现的兼容问题

我们都知道 JS 通过 style 不可以获取行外样式,如果我们需要获取行外样式就会使用这两种

  • IE 下:currentStyle
  • chrome、FF 下:getComputedStyle 第二个参数的作用是获取伪类元素的属性值
  1. 关于“索引”获取字符串每一项出现的兼容性的问题

对于字符串也有类似于数组这样通过下标索引获取每一项的值

  1. 关于使用 firstChild、lastChild 等,获取第一个/最后一个元素节点是产生的问题
  • IE6-8下: firstChild,lastChild,nextSibling,previousSibling 获取第一个元素节点
  • 高版本浏览器IE9+、FF、Chrome:获取的空白文本节点
  1. 关于使用 event 对象,出现兼容性问题

在 IE8 及之前的版本浏览器中,event 事件对象是作为 window 对象的一个属性。

所以兼容的写法如下:

function(event){

event = event || window.event;

}

  1. 关于事件绑定的兼容性问题
  • IE8 以下用: attachEvent('事件名',fn);
  • FF、Chrome、IE9-10 用: attachEventLister('事件名',fn,false);
  1. 关于获取滚动条距离而出现的问题

当我们获取滚动条滚动距离时:

  • IE、Chrome: document.body.scrollTop
  • FF: document.documentElement.scrollTop

兼容处理:

var scrollTop = document.documentElement.scrollTop||document.body.scrollTop

实际使用场景

为什么 console.log(0.2+0.1==0.3) // false*

出现频率: 30%

掌握难度:70分

作用

参考答案

因为浮点数的计算存在 round-off 问题,也就是浮点数不能够进行精确的计算。并且:

  • 不仅 JavaScript,所有遵循 IEEE 754 规范的语言都是如此;

  • 在 JavaScript 中,所有的 Number 都是以 64-bit 的双精度浮点数存储的;

  • 双精度的浮点数在这 64 位上划分为 3 段,而这 3 段也就确定了一个浮点数的值,64bit 的划分是“1-11-52”的模式,具体来说:

    • 就是 1 位最高位(最左边那一位)表示符号位,0 表示正,1 表示负;
    • 11 位表示指数部分;
    • 52 位表示尾数部分,也就是有效域部分

实际使用场景

谈谈你对 JS 执行上下文栈和作用域链的理解

出现频率: 70%

掌握难度:70分

作用

参考答案

什么是执行上下文?

简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

执行上下文的类型

JavaScript 中有三种执行上下文类型。

  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文。

调用栈

调用栈是解析器(如浏览器中的的javascript解析器)的一种机制,可以在脚本调用多个函数时,跟踪每个函数在完成执行时应该返回控制的点。(如什么函数正在执行,什么函数被这个函数调用,下一个调用的函数是谁)

  • 当脚本要调用一个函数时,解析器把该函数添加到栈中并且执行这个函数。
  • 任何被这个函数调用的函数会进一步添加到调用栈中,并且运行到它们被上个程序调用的位置。
  • 当函数运行结束后,解释器将它从堆栈中取出,并在主代码列表中继续执行代码。
  • 如果栈占用的空间比分配给它的空间还大,那么则会导致“栈溢出”错误。

作用域链

当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止,,而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。

实际使用场景

取数组的最大值(ES5、ES6)

出现频率: 30%

掌握难度:30分

作用

参考答案

var arr = [3, 5, 8, 1];

// ES5 方式 console.log(Math.max.apply(null, arr)); // 8

// ES6 方式 console.log(Math.max(...arr)); // 8

实际使用场景

如何判断 img 加载完成

出现频率: 10%

掌握难度:10分

作用

参考答案

为 img DOM 节点绑定 load 事件 readystatechange 事件:readyState 为 complete 和 loaded 则表明图片已经加载完毕。测试 IE6-IE10 支持该事件,其它浏览器不支持。 img 的 complete 属性:轮询不断监测 img 的 complete 属性,如果为 true 则表明图片已经加载完毕,停止轮询。该属性所有浏览器都支持。

实际使用场景

如何阻止冒泡?如何阻止默认事件?

出现频率: 20%

掌握难度:10分

作用

参考答案

// 方法一:IE9+,其他主流浏览器 event.stopPropagation()

// 方法二:火狐未实现 event.cancelBubble = true;

// 方法三:不建议滥用,jq 中可以同时阻止冒泡和默认事件 return false;

// 方法一:全支持 event.preventDefault(); // 方法二:该特性已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。 event.returnValue=false; // 方法三:不建议滥用,jq 中可以同时阻止冒泡和默认事件 return false;

实际使用场景

拖拽会用到哪些事件

出现频率:20%

掌握难度:20分

作用

参考答案

在以前,书写一个拖拽需要用到 mousedown、mousemove、mouseup 这 3 个事件。

HTML5 推出后,新推出了一组拖拽相关的 API,涉及到的事件有 dragstart、dragover、drop 这 3 个事件。

实际使用场景

document.write 和 innerHTML 的区别

出现频率: 10%

掌握难度:20分

作用

参考答案

document.write 是直接写入到页面的内容流,如果在写之前没有调用 document.open, 浏览器会自动调用 open。每次写完关闭之后重新调用该函数,会导致页面全部重绘。 innerHTML 则是 DOM 页面元素的一个属性,代表该元素的 html 内容。你可以精确到某一个具体的元素来进行更改。如果想修改 document 的内容,则需要修改 document.documentElement.innerElement。 innerHTML 很多情况下都优于 document.write,其原因在于不会导致页面全部重绘。


document.writeinnerHTML 都能将 HTML 字符串解析为 DOM 树,再将 DOM 树插入到某个位置,但两种在执行细节上还是有许多不同。

1)write() 方法存在于 Document 对象中,innerHTML 属性存在于 Element 对象中;

2)document.write 会将解析后的 DOM 树插入到文档中调用它的脚本元素的位置,而 innerHTML 会将 DOM 树插入到指定的元素内;

3)document.write 会将多次调用的字符串参数自动连接起来,innerHTML 要用赋值运算符 "+=" 拼接;

4)只有当文档还在解析时,才能使用 document.write,否则 document.write 的值会将当前文档覆盖掉,而 innerHTML 属性则没有这个限制;

实际使用场景

clientWidth,offsetWidth,scrollWidth 的区别

出现频率: 40%

掌握难度:40分

作用

参考答案

clientWidth = width+左右 padding

offsetWidth = width + 左右 padding + 左右 boder

scrollWidth:获取指定标签内容层的真实宽度(可视区域宽度+被隐藏区域宽度)。

实际使用场景

continue 和 break 的区别

出现频率: 40%

掌握难度:10分

作用

参考答案

break:用于永久终止循环。即不执行本次循环中 break 后面的语句,直接跳出循环。
continue:用于终止本次循环。即本次循环中 continue 后面的代码不执行,进行下一次循环的入口判断。

实际使用场景

async/await 如何捕获错误

出现频率: 30%

掌握难度:30分

作用

参考答案

可以使用 try...catch 来进行错误的捕获

async function test() {

try {
    const res = await test1()
} catch (err) {
    console.log(err)
}
console.log("test")

}

实际使用场景

写出代码的执行结果,并解释为什么?

出现频率: 10%

掌握难度:50分

作用

参考答案

function a() {

console.log(1);

} (function() {

if (false) {
    function a() {
        console.log(2);
    }
}
console.log(typeof a); 
a(); 

})()

会报错,a is not a function。 因为立即执行函数里面有函数 a,a 会被提升到该函数作用域的最顶端,但是由于判断条件是 false,所以不会进入到条件语句里面, a 也就没有值。所以 typeof 打印出来是 undefined。而后面在尝试调用方法,自然就会报错。

实际使用场景

alert(a)、阻断线程执行、变量提升

出现频率: 40%

掌握难度:50分

作用

参考答案

alert(a);

a();

var a = 3;

function a() {

alert(10);

};

alert(a);

a = 6;

a();

首先打印 function a() {alert(10);};

然后打印 10

最后打印 3

解析: 首先 a 变量会被提升到该全局作用域的最顶端,然后值为对应的函数,所以第一次打印出来的是函数。

接下来调用这个 a 函数,所以打印出 10

最后给这个 a 赋值为 3,然后又 alert,所以打印出 3。

之后 a 的值还会发生改变,但是由于没有 alert,说明不会再打印出其他值了。

实际使用场景

请简述 ES6 代码转成 ES5 代码的实现思路。

出现频率: 40%

掌握难度:80分

作用

参考答案

说到 ES6 代码转成 ES5 代码,我们肯定会想到 Babel。所以,我们可以参考 Babel 的实现方式。 那么 Babel 是如何把 ES6 转成 ES5 呢,其大致分为三步:

将代码字符串解析成抽象语法树,即所谓的 AST 对 AST 进行处理,在这个阶段可以对 ES6 代码进行相应转换,即转成 ES5 代码 根据处理后的 AST 再生成代码字符串

实际使用场景

eval 是做什么的?

出现频率: 10%

掌握难度:10分

作用

参考答案

此函数可以接受一个字符串 str 作为参数,并把此 str 当做一段 javascript 代码去执行,如果 str 执行结果是一个值则返回此值,否则返回 undefined。如果参数不是一个字符串,则直接返回该参数。

例如:

eval("var a=1");//声明一个变量a并赋值1。

eval("2+3");//5执行加运算,并返回运算值。

eval("mytest()");//执行mytest()函数。

eval("{b:2}");//声明一个对象。

实际使用场景

map 和 forEach 的区别?

出现频率: 20%

掌握难度:20分

作用

参考答案

两者区别

forEach()方法不会返回执行结果,而是undefined。

也就是说,forEach()会修改原来的数组。而map()方法会得到一个新的数组并返回。

适用场景

forEach适合于你并不打算改变数据的时候,而只是想用数据做一些事情 – 比如存入数据库或则打印出来。

map()适用于你要改变数据值的时候。不仅仅在于它更快,而且返回一个新的数组。这样的优点在于你可以使用复合(composition)(map, filter, reduce 等组合使用)来玩出更多的花样。

实际使用场景

Array 的常用方法

出现频率: 30%

掌握难度:50分

作用

参考答案

Array 的常用方法很多,挑选几个自己在实际开发中用的比较多的方法回答即可。

2c66c11f3fe14000b1ed56b4d40f5f20~tplv-k3u1fbpfcp-zoom-in-crop-mark 1512 0 0 0.webp

实际使用场景

什么是预解析(预编译)

出现频率: 40%

掌握难度:70分

作用

参考答案

所谓的预解析(预编译)就是:在当前作用域中,JavaScript 代码执行之前,浏览器首先会默认的把所有带 var 和 function 声明的变量进行提前的声明或者定义。 另外,var 声明的变量和 function 声明的函数在预解析的时候有区别,var 声明的变量在预解析的时候只是提前的声明,function 声明的函数在预解析的时候会提前声明并且会同时定义。也就是说 var 声明的变量和 function 声明的函数的区别是在声明的同时有没有同时进行定义。

实际使用场景

冒泡排序的思路,不用 sort

出现频率: 30%

掌握难度:60分

作用

参考答案

var examplearr = [8, 94, 15, 88, 55, 76, 21, 39];

function sortarr(arr) {

for (i = 0; i < arr.length - 1; i++) {
    for (j = 0; j < arr.length - 1 - i; j++) {
        if (arr[j] > arr[j + 1]) {
            var temp = arr[j];
            arr[j] = arr[j + 1];
            arr[j + 1] = temp;
        }
    }
}
return arr;

}

sortarr(examplearr);

console.log(examplearr); // [8, 15, 21, 39, 55, 76, 88, 94]

实际使用场景

symbol 用途

出现频率: 40%

掌握难度:70分

作用

参考答案

可以用来表示一个独一无二的变量防止命名冲突。但是面试官问还有吗?我没想出其他的用处就直接答我不知道了,还可以利用 symbol 不会被常规的方法(除了 Object.getOwnPropertySymbols 外)遍历到,所以可以用来模拟私有变量。 主要用来提供遍历接口,布置了 symbol.iterator 的对象才可以使用 for···of 循环,可以统一处理数据结构。调用之后回返回一个遍历器对象,包含有一个 next 方法,使用 next 方法后有两个返回值 value 和 done 分别表示函数当前执行位置的值和是否遍历完毕。 Symbol.for() 可以在全局访问 symbol

实际使用场景

什么是函数式编程,应用场景是什么

出现频率: 10%

掌握难度:70分

作用

参考答案

函数式编程和面向对象编程一样,是一种编程范式。强调执行的过程而非结果,通过一系列的嵌套的函数调用,完成一个运算过程。 它主要有以下几个特点:

函数是"一等公民":函数优先,和其他数据类型一样。 只用"表达式",不用"语句":通过表达式(expression)计算过程得到一个返回值,而不是通过一个语句(statement)修改某一个状态。 无副作用:不污染变量,同一个输入永远得到同一个数据。 不可变性:前面一提到,不修改变量,返回一个新的值。

函数式编程的概念其实出来也已经好几十年了,我们能在很多编程语言身上看到它的身影。比如比较纯粹的 Haskell,以及一些语言开始逐渐成为多范式编程语言,比如 Swift,还有 Kotlin,Java,Js 等都开始具备函数式编程的特性。 函数式编程在前端的应用场景

Stateless components:React 在 0.14 之后推出的无状态组件 Redux

函数式编程在后端的应用场景

Lambda 架构

实际使用场景

JS 小数不精准,如何计算

出现频率: 30%

掌握难度:60分

作用

参考答案

方法一:指定要保留的小数位数(0.1+0.2).toFixed(1) = 0.3;这个方法toFixed是进行四舍五入的也不是很精准,对于计算金额这种严谨的问题,不推荐使用,而且不同浏览器对toFixed的计算结果也存在差异。 方法二:把需要计算的数字升级(乘以10的n次幂)成计算机能够精确识别的整数,等计算完毕再降级(除以10的n次幂),这是大部分编程语言处理精度差异的通用方法。

实际使用场景

手写发布订阅(头条2020)

出现频率: 50%

掌握难度:90分

作用

参考答案

juejin.cn/post/715904…

实际使用场景

JavaScript 是如何运行的?解释型语言和编译型语言的差异是什么?

出现频率: 20%

掌握难度:50分

作用

参考答案

关于第一个问题,这不是三言两语或者几行文字就能够讲清楚的,这里放上一篇博文地址: segmentfault.com/a/119000001… 第二个问题:解释型语言和编译型语言的差异是什么? 电脑能认得的是二进制数,不能够识别高级语言。所有高级语言在电脑上执行都需要先转变为机器语言。但是高级语言有两种类型:编译型语言和解释型语言。常见的编译型语言语言有C/C++、Pascal/Object 等等。常见的解释性语言有python、JavaScript等等。 编译型语言先要进行编译,然后转为特定的可执行文件,这个可执行文件是针对平台的(CPU类型),可以这么理解你在PC上编译一个C源文件,需要经过预处理,编译,汇编等等过程生成一个可执行的二进制文件。当你需要再次运行改代码时,不需要重新编译代码,只需要运行该可执行的二进制文件。优点,编译一次,永久执行。还有一个优点是,你不需要提供你的源代码,你只需要发布你的可执行文件就可以为客户提供服务,从而保证了你的源代码的安全性。但是,如果你的代码需要迁移到linux、ARM下时,这时你的可执行文件就不起作用了,需要根据新的平台编译出一个可执行的文件。这也就是多个平台需要软件的多个版本。缺点是,跨平台能力差。 解释型语言需要一个解释器,在源代码执行的时候被解释器翻译为一个与平台无关的中间代码,解释器会把这些代码翻译为及其语言。打个比方,编译型中的编译相当于一个翻译官,它只能翻译英语,而且中文文章翻译一次就不需要重新对文章进行二次翻译了,但是如果需要叫这个翻译官翻译德语就不行了。而解释型语言中的解释器相当于一个会各种语言的机器人,而且这个机器人回一句一句的翻译你的语句。对于不同的国家,翻译成不同的语言,所以,你只需要带着这个机器人就可以。解释型语言的有点是,跨平台,缺点是运行时需要源代码,知识产权保护性差,运行效率低。

实际使用场景

列举你所了解的编程范式?

出现频率: 10%

掌握难度:20分

作用

参考答案

编程范式 Programming paradigm 是指计算机中编程的典范模式或方法。 常见的编程范式有:函数式编程、程序编程、面向对象编程、指令式编程等。 不同的编程语言也会提倡不同的“编程范型”。一些语言是专门为某个特定的范型设计的,如 Smalltalk 和 Java 支持面向对象编程。而 Haskell 和 Scheme 则支持函数式编程。现代编程语言的发展趋势是支持多种范型,例如 ES 支持函数式编程的同时也支持面向对象编程。

实际使用场景

什么是面向切面(AOP)的编程?

出现频率:10%

掌握难度:70分

作用

参考答案

juejin.cn/post/715904…

实际使用场景

扩展运算符的作用及使用场景

出现频率: 30%

掌握难度:30分

作用

参考答案

扩展运算符是三个点(...),主要用于展开数组,将一个数组转为参数序列。

扩展运算符使用场景:

  • 代替数组的 apply 方法
  • 合并数组
  • 复制数组
  • argumentsNodeList 转为数组
  • 与解构赋值结合使用
  • 将字符串转为数组

function getAge(...args) {
  console.log(typeof args);
}

getAge(21);//"object"
  • A: "number"
  • B: "array"
  • C: "object"
  • D: "NaN"

ES6 中的不定参数(…args)返回的是一个数组。

typeof 检查数组的类型返回的值是 object

实际使用场景

Proxy 可以实现什么功能?

出现频率: 50%

掌握难度:40分

作用

参考答案

ProxyES6 中新增的一个特性。Proxy 让我们能够以简洁易懂的方式控制外部对对象的访问。其功能非常类似于设计模式中的代理模式。

Proxy 在目标对象的外层搭建了一层拦截,外界对目标对象的某些操作,必须通过这层拦截。

使用 Proxy 的好处是对象只需关注于核心逻辑,一些非核心的逻辑(如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,需要验证;某些属性的访问控制等)可以让 Proxy 来做。从而达到关注点分离,降级对象复杂度的目的。

Proxy 的基本语法如下:

var proxy = new Proxy(target, handler);

通过构造函数来生成 Proxy 实例,构造函数接收两个参数。target 参数是要拦截的目标对象,handler 参数也是一个对象,用来定制拦截行为。

Vue 3.0 主要就采用的 Proxy 特性来实现响应式,相比以前的 Object.defineProperty 有以下优点:

  • 可以劫持整个对象,并返回一个新的对象
  • 13 种劫持操作

juejin.cn/post/715289…

实际使用场景

对象与数组的解构的理解

出现频率: 50%

掌握难度:30分

作用

参考答案

解构是 ES6 的一种语法规则,可以将一个对象或数组的某个属性提取到某个变量中。

解构对象示例:

// var/let/const{属性名}=被解构的对象 const user = { name: "abc", age: 18, sex: "男", address: { province: "重庆", city: "重庆" } } let { name, age, sex, address} = user; console.log(name, age, sex, address);

解构数组示例:

const [a, b, c] = [1, 2, 3];

实际使用场景

如何提取高度嵌套的对象里的指定属性?

出现频率: 10%

掌握难度:50分

作用

参考答案

juejin.cn/post/715904…

实际使用场景

Unicode、UTF-8、UTF-16、UTF-32 的区别?

出现频率: 10%

掌握难度:50分

作用

参考答案

Unicode 为世界上所有字符都分配了一个唯一的数字编号,这个编号范围从 0x0000000x10FFFF (十六进制),有 110 多万,每个字符都有一个唯一的 Unicode 编号,这个编号一般写成 16 进制,在前面加上 U+。例如:“马”的 UnicodeU+9A6CUnicode 就相当于一张表,建立了字符与编号之间的联系。

f3797ac582384f339790c8d9a9ea2326~tplv-k3u1fbpfcp-zoom-in-crop-mark 1512 0 0 0.webp

Unicode 本身只规定了每个字符的数字编号是多少,并没有规定这个编号如何存储。

那我们可以直接把 Unicode 编号直接转换成二进制进行存储,怎么对应到二进制表示呢?

Unicode 可以使用的编码有三种,分别是:

  • UFT-8:一种变长的编码方案,使用 1~6 个字节来存储;
  • UFT-32:一种固定长度的编码方案,不管字符编号大小,始终使用 4 个字节来存储;
  • UTF-16:介于 UTF-8UTF-32 之间,使用 2 个或者 4 个字节来存储,长度既固定又可变。

实际使用场景

为什么函数的 arguments 参数是类数组而不是数组?如何遍历类数组?

出现频率: 10%

掌握难度:30分

作用

参考答案

首先了解一下什么是数组对象和类数组对象。

数组对象:使用单独的变量名来存储一系列的值。从 Array 构造函数中继承了一些用于进行数组操作的方法。

例如:

var mycars = new Array(); mycars[0] = "zhangsan"; mycars[1] = "lisi"; mycars[2] = "wangwu";

类数组对象:对于一个普通的对象来说,如果它的所有 property 名均为正整数,同时也有相应的length属性,那么虽然该对象并不是由Array构造函数所创建的,它依然呈现出数组的行为,在这种情况下,这些对象被称为“类数组对象”。

两者区别

  • 一个是对象,一个是数组
  • 数组的length属性,当新的元素添加到列表中的时候,其值会自动更新。类数组对象的不会。
  • 设置数组的length属性可以扩展或截断数组。
  • 数组也是Array的实例可以调用Array的方法,比如push、pop等等

所以说arguments对象不是一个 Array 。它类似于Array,但除了length属性和索引元素之外没有任何Array属性。

可以使用 for...in 来遍历 arguments 这个类数组对象。

实际使用场景

escape、encodeURI、encodeURIComponent 的区别

出现频率: 20%

掌握难度:40分

作用

参考答案

escape 除了 ASCII 字母、数字和特定的符号外,对传进来的字符串全部进行转义编码,因此如果想对 URL 编码,最好不要使用此方法。

encodeURI 用于编码整个 URI,因为 URI 中的合法字符都不会被编码转换。

encodeURIComponent 方法在编码单个URIComponent(指请求参数)应当是最常用的,它可以讲参数中的中文、特殊字符进行转义,而不会影响整个 URL

实际使用场景

use strict 是什么意思 ? 使用它区别是什么?

出现频率: 30%

掌握难度:10分

作用

参考答案

use strict 代表开启严格模式,这种模式使得 Javascript 在更严格的条件下运行,实行更严格解析和错误处理。

开启“严格模式”的优点:

  • 消除 Javascript 语法的一些不合理、不严谨之处,减少一些怪异行为;
  • 消除代码运行的一些不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的 Javascript 做好铺垫。

function getAge() {
  "use strict";
  age = 21;
  console.log(age);
}

getAge();//ReferenceError
  • A: 21
  • B: undefined
  • C: ReferenceError
  • D: TypeError

分析:

"use strict" 严格模式中,使用未声明的变量会引发报错。

实际使用场景

下面代码的输出是什么?( C

出现频率: 30%

掌握难度:50分

作用

参考答案

let number = 0;
console.log(number++);
console.log(++number);
console.log(number);
  • A: 1 1 2
  • B: 1 2 2
  • C: 0 2 2
  • D: 0 1 2

分析:

++ 后置时,先输出,后加 1;++ 前置时,先加 1,后输出;

第一次输出的值为 0,输出完成后 number1 变为 1

第二次输出,number 先加 1 变为 2,然后输出值 2

第三次输出,number 值没有变化,还是 2

实际使用场景

错误处理机制

出现频率: 30%

掌握难度:60分

作用

参考答案

下面代码的输出是什么?( A

(() => {
  let x, y;
  try {
    throw new Error();
  } catch (x) {
    (x = 1), (y = 2);
    console.log(x);
  }
  console.log(x);
  console.log(y);
})();
  • A: 1 undefined 2
  • B: undefined undefined undefined
  • C: 1 1 2
  • D: 1 undefined undefined

分析:

catch 块接收参数 x。当我们传递参数时,这与变量的 x 不同。这个变量 x 是属于 catch 作用域的。

之后,我们将这个块级作用域的变量设置为 1,并设置变量 y 的值。 现在,我们打印块级作用域的变量 x,它等于 1

catch 块之外,x 仍然是 undefined,而 y2。 当我们想在 catch 块之外的 console.log(x) 时,它返回undefined,而 y 返回 2

实际使用场景

前端为什么提倡模块化开发?

出现频率: 50%

掌握难度:30分

作用

参考答案

模块化能将一个复杂的大型系统分解成一个个高内聚、低耦合的简单模块,并且每个模块都是独立的,用于完成特定的功能。模块化后的系统变得更加可控、可维护、可扩展,程序代码也更简单直观,可读性也很高,有利于团队协作开发。ES6 模块化的出现,使得前端能更容易、更快速的实现模块化开发。

实际使用场景

跨域

出现频率: 60%

掌握难度:60分

作用

参考答案

*JSONP(JSON with padding)*是一种借助 script 元素实现跨域的技术,它不会使用 XHR 对象。之所以能实现跨域,主要是因为 script 元素有以下两个特点:

1)它的 src 属性能够访问任何 URL 资源,不会受同源策略的限制;

2)如果访问的资源包含 JavaScript 代码,那么在下载下来后会自动执行;

JSONP 就是基于这两点,再与服务器配合来实现跨域请求的,它的执行步骤可分为以下 6 步:

1)定义一个回调函数;

2)用 DOM 方法动态创建一个 script 元素;

3)通过 script 元素的 src 属性指定要请求的 URL,并且将回调函数的名称作为一个参数传递过去;

4)将 script 元素插入到当前文档中,开始请求;

5)服务器接收到传递过来的参数,然后将回调函数和数据以调用的形式输出;

6)当 script 元素接收到响应中的脚本代码后,就会自动的执行它们;


实际使用场景

请指出 document.onload 和 document.ready 两个事件的区别

出现频率: 10%

掌握难度:30分

作用

参考答案

页面加载完成有两种事件:一是 *ready*,表示文档结构已经加载完成(不包含图片等非文字媒体文件);二是 *onload*,指示页面包含图片等文件在内的所有元素都加载完成。

实际使用场景

如何编写高性能的 JavaScript

出现频率: 50%

掌握难度:50分

作用

参考答案

  • 遵循严格模式:"use strict"

  • JavaScript 本放在页面底部,加快渲染页面

  • JavaScript 脚本将脚本成组打包,减少请求

  • 使用非阻塞方式下载 JavaScript 脚本

  • 尽量使用局部变量来保存全局变量

  • 尽量减少使用闭包

  • 使用 window 对象属性方法时,省略 window

  • 尽量减少对象成员嵌套

  • 缓存 DOM 节点的访问

  • 通过避免使用 eval()Function() 构造器

  • setTimeout()setInterval() 传递函数而不是字符串作为参数

  • 尽量使用直接量创建对象和数组

  • 最小化重绘 (repaint) 和回流 (reflow)


  • 垃圾回收
  • 闭包中的对象清楚
  • 防抖节流
  • 分批加载(setInterval,加载10000个节点)
  • 事件委托
  • 少用with
  • requestAnimationFrame的使用
  • script标签中的defer和async
  • CDN

实际使用场景

什么是执行栈,什么是执行上下文?

出现频率: 50%

掌握难度:80分

作用

参考答案

执行上下文分为:

全局执行上下文
    创建一个全局的window对象,并规定this指向window,执行js的时候就压入栈底,关闭浏览器的时候才弹出
函数执行上下文
    每次函数调用时,都会新创建一个函数执行上下文
    执行上下文分为创建阶段和执行阶段
        创建阶段:函数环境会创建变量对象:arguments对象(并赋值)、函数声明(并赋值)、变量声明(不赋值),函数表达式声明(不赋值);会确定this指向;会确定作用域
        执行阶段:变量赋值、函数表达式赋值,使变量对象编程活跃对象
eval执行上下文

执行栈:

  • 首先栈特点:先进后出
  • 当进入一个执行环境,就会创建出它的执行上下文,然后进行压栈,当程序执行完成时,它的执行上下文就会被销毁,进行弹栈。
  • 栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文
  • 只有浏览器关闭的时候全局执行上下文才会弹出

实际使用场景

为什么JS是单线程的?

出现频率: 50%

掌握难度:40分

作用

参考答案

因为JS里面有可视的Dom,如果是多线程的话,这个线程正在删除DOM节点,另一个线程正在编辑Dom节点,导致浏览器不知道该听谁的

实际使用场景

如何实现异步编程?

出现频率: 10%

掌握难度:10分

作用

参考答案

回调函数

实际使用场景

Generator是怎么样使用的以及各个阶段的变化如何?

出现频率: 10%

掌握难度:70分

作用

参考答案

首先生成器是一个函数,用来返回迭代器的

调用生成器后不会立即执行,而是通过返回的迭代器来控制这个生成器的一步一步执行的

通过调用迭代器的next方法来请求一个一个的值,返回的对象有两个属性,一个是value,也就是值;另一个是done,是个布尔类型,donetrue说明生成器函数执行完毕,没有可返回的值了,

donetrue后继续调用迭代器的next方法,返回值的value为undefined

状态变化:

每当执行到yield属性的时候,都会返回一个对象
这时候生成器处于一个非阻塞的挂起状态
调用迭代器的next方法的时候,生成器又从挂起状态改为执行状态,继续上一次的执行位置执行
直到遇到下一次yield依次循环
直到代码没有yield了,就会返回一个结果对象done为true,value为undefined

实际使用场景

说说 Promise 的原理?你是如何理解 Promise 的?

出现频率: 70%

掌握难度:90分

作用

参考答案

做到会写简易版的promise和all函数就可以

class MyPromise2 {
      constructor(executor) {
        // 规定状态
        this.state = "pending"
        // 保存 `resolve(res)` 的res值
        this.value = undefined
        // 保存 `reject(err)` 的err值
        this.reason = undefined
        // 成功存放的数组
        this.successCB = []
        // 失败存放的数组
        this.failCB = []


        let resolve = (value) => {
          if (this.state === "pending") {
            this.state = "fulfilled"
            this.value = value
            this.successCB.forEach(f => f())
          }
        }
        let reject = (reason) => {
          if (this.state === "pending") {
            this.state = "rejected"
            this.value = value
            this.failCB.forEach(f => f())
          }
        }

        try {
          // 执行
          executor(resolve, reject)
        } catch (error) {
          // 若出错,直接调用reject
          reject(error)
        }
      }
      then(onFulfilled, onRejected) {
        if (this.state === "fulfilled") {
          onFulfilled(this.value)
        }
        if (this.state === "rejected") {
          onRejected(this.value)
        }
        if (this.state === "pending") {
          this.successCB.push(() => { onFulfilled(this.value) })
          this.failCB.push(() => { onRejected(this.reason) })
        }
      }
    }


    Promise.all = function (promises) {
      let list = []
      let count = 0
      function handle(i, data) {
        list[i] = data
        count++
        if (count == promises.length) {
          resolve(list)
        }
      }
      return Promise((resolve, reject) => {
        for (let i = 0; i < promises.length; i++) {
          promises[i].then(res => {
            handle(i, res)
          }, err => reject(err))
        }
      })
    }

实际使用场景

变量和函数怎么进行提升的?优先级是怎么样的?

出现频率: 30%

掌握难度:70分

作用

参考答案

  • 对所有函数声明进行提升(除了函数表达式和箭头函数),引用类型的赋值

    • 开辟堆空间
    • 存储内容
    • 将地址赋给变量
  • 对变量进行提升,只声明,不赋值,值为undefined

实际使用场景