JS面试题整理(用自己语言方式)

256 阅读58分钟

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

出现频率: 80%

掌握难度:20分

作用

参考答案

变量提升(声明赋值前访问var是undifind,let\const是引用错误,函数声明也会被提升,但是被提升到了最顶端,提前可以正常调用函数,所以将位于所有变量声明之上。)、块作用域、重复声明赋值

  1. var 定义的变量,a没有块的概念,可以跨块访问, 不能跨函数访问,有变量提升。
  2. let 定义的变量,a有块的概念, 不能跨块访问,不能跨函数访问,无变量提升,不可以重复声明。
  3. const 用来定义常量,使用时必须初始化(即必须赋值),有块作用域,而且不能修改(如果使用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 的报错。

变量和函数怎么进行提升的?优先级是怎么样的?

  • 对所有函数声明进行提升(除了函数表达式和箭头函数),引用类型的赋值

    • 开辟堆空间
    • 存储内容
    • 将地址赋给变量
  • 对变量进行提升,只声明,不赋值,值为undefined

什么是执行栈,什么是执行上下文?

执行上下文分为:

juejin.cn/post/729702…

全局执行上下文
    创建一个全局的window对象,并规定this指向window,执行js的时候就压入栈底,关闭浏览器的时候才弹出
函数执行上下文
    每次函数调用时,都会新创建一个函数执行上下文
    执行上下文分为创建阶段和执行阶段
        创建阶段:函数环境会创建变量对象:arguments对象(并赋值)、函数声明(并赋值)、变量声明(不赋值),函数表达式声明(不赋值);会确定this指向;会确定作用域
        执行阶段:变量赋值、函数表达式赋值,使变量对象编程活跃对象
eval执行上下文

执行栈:

  • 首先栈特点:先进后出
  • 当进入一个执行环境,就会创建出它的执行上下文,然后进行压栈,当程序执行完成时,它的执行上下文就会被销毁,进行弹栈。
  • 栈底永远是全局环境的执行上下文,栈顶永远是正在执行函数的执行上下文
  • 只有浏览器关闭的时候全局执行上下文才会弹出

实际使用场景

什么是预解析(预编译)

出现频率: 20%

掌握难度:20分

作用

参考答案

juejin.cn/post/729466…

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

实际使用场景

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

出现频率: 80%

掌握难度:50分

作用

参考答案

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

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

stringsymbolnumberbooleanundefinednull

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

虽然 NaN 表示非数,但是它却属于 number 类型。

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

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

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

典型用法是:

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

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

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

object

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

两者的区别在于:

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

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

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

数据类型的转换:

blog.csdn.net/good_moring…

blog.csdn.net/good_moring…

blog.csdn.net/lai123wei/a…

blog.csdn.net/qq_23365135…

blog.csdn.net/CRMEB/artic…

显示转换:

  • 转换为 booleanBoolean()

Boolean() :把null,undefind,false,0(包括+0、-0),NAN,空字符串转换为false;其余的转换为true。

  • 转换为 numberNumber()parseInt()parseFloat()

Number()内部调用parseInt()parseFloat()parseInt()parseFloat()、*parseFloat()*只转换有效数字字符,其余皆返回NaN;parseInt() 支持多进制转换,*parseFloat()*不支持多进制转换;*Number()*进行完整字符转换。

**Number():**转换规则

·  如果是 null,返回 0;

·  如果是 undefined,返回 NaN;

如果是布尔值,true 和 false 分别被转换为 1 和 0;

如果是数字,返回自身;

如果是字符串,遵循以下规则:如果字符串中只包含数字(或者是 0X / 0x 开头的十六进制数字字符串,允许包含正负号),则将其转换为十进制;如果字符串中包含有效的浮点格式,将其转换为浮点数值;如果是空字符串,将其转换为 0;如果不是以上格式的字符串,均返回 NaN;

如果是 Symbol,抛出错误;

如果是对象,并且部署了 [Symbol.toPrimitive] ,那么调用此方法,否则调用对象的 valueOf() 方法,然后依据前面的规则转换返回的值(没有返回值则返回值是undefined);如果转换的结果是 NaN ,则调用对象的 toString() 方法,再次依照前面的顺序转换返回对应的值(Object 转换规则会在下面细讲)。

  • 转换为 stringString()toString()

要执行这种强制类型转换,只需要调用作为参数传递进来的值的 toString()方法,即把1转换成   "1 ",把true转换成 "true ",把false转换成 "false ",依此类推。

强制转换成字符串和调用toString()方法的唯一不同之处在于,对null或undefined值强制类型转换可以生成字符串而不引 发错误:

  1. var    s1    =    String(null);   //"null"   
  2. var    oNull    =   null;   
  3. var    s2    =    oNull.toString();   //won’t    work,    causes    an    error

隐式转换:本质还是调用的显示转换的规则

凡是通过逻辑运算符 (&&、 ||、 !)、运算符 (+、-、*、/)、关系操作符 (>、 <、 <= 、>=)、相等运算符 (==) 或者 if/while 条件的操作,如果遇到两个数据类型不一样的情况,都会出现隐式类型转换。这里你需要重点关注一下,因为比较隐蔽,特别容易让人忽视。

下面着重讲解一下日常用得比较多的“==”和“+”这两个符号的隐式转换规则。

对象的隐式转换规则:

· 如果部署了 Symbol.toPrimitive 方法,优先调用再返回;

· 调用valueOf(),如果转换为基础类型,则返回;

· 调用 toString(),如果转换为基础类型,则返回;

· 如果都没有返回基础类型,会报错。

”==“隐式转换:(转换为数字类型优先)

· 如果类型相同,无须进行类型转换;

· 如果其中一个操作值是 null 或者 undefined,那么另一个操作符必须为 null 或者 undefined,才会返回 true,否则都返回 false;

· 如果其中一个是 Symbol 类型,那么返回 false;

· 两个操作值如果为 string 和 number 类型,那么就会将字符串转换为 number;

· 如果一个操作值是 boolean,那么转换成 number;

· 如果一个操作值为 object 且另一方为 string、number 或者 symbol,就会把 object 转为原始类型再进行判断(调用 object 的 valueOf/toString方法进行转换)。

”+“隐式转换: (遇到字符串字符串优先转为字符串,遇到数字优先转为数字,同时遇到字符串和数字则转为字符串)

·  如果其中有一个是字符串,另外一个是 undefined、null 或布尔型,则调用 toString() 方法进行字符串拼接;如果是纯对象、数组、正则等,则默认调用对象的转换方法会存在优先级(下一讲会专门介绍),然后再进行拼接。

·  如果其中有一个是数字,另外一个是 undefined、null、布尔型或数字,则会将其转换成数字进行加法运算,对象的情况还是参考上一条规则。

·  如果其中一个是字符串、一个是数字,则按照字符串规则进行拼接。

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

显示转换:

  • 转换为 numberNumber()parseInt()parseFloat()
  • 转换为 stringString()toString()
  • 转换为 booleanBoolean()

隐式转换:

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

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

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

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

两边类型相同,比较大小

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

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

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

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

数据类型的检测:

var=[s,f]

Object.prototype.toString.call(var)://[object Array]对于自定义类型的对象无法识别自定义类型,返回"[object Object]";想要获取自定义类型可以obj.constructor

typeof var ://object对于null、对象、内置对象、数组都返回字符串object;

var instanceof Array :// true

instanceof 实现原理

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

instanceof 用于检测对象 A 是不是 B 的实例,而检测是基于原型链进行查找的,也就是说 B 的 prototype 有没有在对象 A 的__proto__ 原型链上;需要注意的是,instanceof 只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型。

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

var.constructor === Array): // true

Array.isArray(var): //true

为什么typeof null是Object

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

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

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

数据类型的深浅拷贝

blog.csdn.net/m0_61696809…

浅拷贝:只复制栈内存中的数据;如果是基本数据类型拷贝前后的数据相互独立、不相互影响;如果是引用类型数据只复制栈内存中对象地址引用,堆内存中的对象数据无法复制,拷贝前后的数据相互影响,因为共用一块堆内存。

浅拷贝方式: js中没有原生的深拷贝方式,都是浅拷贝方式;如:(“=”赋值不是浅拷贝方式,第一层就相互影响)、Object.assign(目标对象,源对象1,源对象2.....)、arr1.concat(arr2)、arr1.splice()、arr.scice()、ES6 扩展运算符。

深拷贝:不仅赋值栈内存中的数据,也会复制堆内存中的对象数据,所以拷贝前后的数据相互独立互不影响。

深拷贝方式:

JSON.stringfy()/JSON.parse():但对于函数类型不能进行深拷贝。

递归拷贝

 **// 方法 1** 
    function deepClone (obj) {
      //  通过 constructor 反射  创建实例
      let ObjClone = new obj.constructor()
      if (obj && typeof (obj) === 'object') {
        // 遍历原来的对象
        for (key in obj) {
>          // 判断对象的属性是不是本身的属性(不能是通过原型链继承获取过来),如果是则进行下一步
          if (obj.hasOwnProperty(key)) {
            // 对象的每一位不能为空 为空无法进行拷贝
            if (obj[key] && typeof (obj[key]) === 'object') {
            // 如果是复杂数据类型 则进行递归深拷贝 否则则进行浅拷贝
              ObjClone[key] = deepClone(obj[key])
            }
            else {
            // 浅拷贝
              ObjClone[key] = obj[key]
            }
          }
        }
      }
      
      return ObjClone
    }
    // 将 拷贝的对象赋值 
    const NewsObject = deepClone(OldObj)
    // 更改 复杂数据类型数据
    NewsObject.Around.page = 1000
    // 源对象 
    console.log(OldObj);
    // 新对象
    console.log(NewsObject);

练习题:

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

是因为计算过程中,先由十进制转化为二进制,再由二进制转化为u十进制,出现了精度失真、取近似值导致的计算误差。

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

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

分析:

++ 后置时,先输出(先赋值再做加运算,number=number,number=number+1),后加 1;++ 前置时,先加 1,后输出(先赋值再做加运算,number=number+1);

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

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

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

下面的代码输出什么?

var a = function () { return 5 }
a.toString = function () { return 3 }
console.log(a + 7);

参考答案:

10

因为会自动调用 a 函数的 toString 方法。

分析以下代码的执行结果并解释为什么。

var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x) 	
console.log(b.x)

运行结果:

undefined{n: 2}

分析:

首先,ab 同时引用了 {n: 1} 对象,接着执行到 a.x = a = {n: 2} 语句,虽然赋值是从右到左执行,但是点(.)的优先级比赋值符(=)要高,所以这里首先执行 a.x,相当于为 a(或者 b)所指向的 {n:1} 对象新增了一个属性 x,即此时对象将变为 {n: 1; x: undefined} 。然后按正常情况,从右到左进行赋值,此时执行 a = {n: 2} 的时候,a的引用改变,指向了新对象 {n: 2} ,而 b 依然指向的是旧对象 {n: 1; x: undefined} 。之后再执行 a.x = {n: 2} 的时候,并不会重新解析一遍 a,而是沿用最初解析 a.x 时候的 a,即旧对象 {n: 1; x: undefined} ,故此时旧对象的 x 的值变为*{n: 2}*,旧对象为 {n: 1; x: {n: 2}} ,它依然被 b 引用着。

最后,a 指向的对象为 {n: 2}b 指向的对象为 {n: 1; x: {n: 2}} 。因此输出 a.x 值为 undefined,输出 b.x 值为 {n: 2}

JS 小数不精准,如何计算

方法一:指定要保留的小数位数(0.1+0.2).toFixed(1) = 0.3;这个方法toFixed是进行四舍五入的也不是很精准,对于计算金额这种严谨的问题,不推荐使用,而且不同浏览器对toFixed的计算结果也存在差异。

方法二:把需要计算的数字升级(乘以10的n次幂)成计算机能够精确识别的整数,等计算完毕再降级(除以10的n次幂),这是大部分编程语言处理精度差异的通用方法。

实际使用场景

数组去重方法

出现频率: 70%

掌握难度:50分

作用

参考答案

// 利用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;
}

//利用reduce

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

实际使用场景

取数组的最大值

出现频率: 20%

掌握难度:10分

作用

参考答案

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

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

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

Array 的常用方法

出现频率: 50%

掌握难度:50分

作用

参考答案

c48ea8dc84b94956b03651b5dce45b8e~tplv-k3u1fbpfcp-jj-mark 3024 0 0 0 q75.webp

# for of 和 for in 的区别

出现频率: 10%

掌握难度:30分

作用

参考答案blog.csdn.net/qq_43886365…

为什么函数的 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 这个类数组对象。

实际使用场景

代理Proxy

出现频率: 50%

掌握难度:50分

作用

参考答案

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

实际使用场景: Vue的双向绑定 vue2用的是Object.defineProperty,vue3用的是proxy

symbol 用途

出现频率:10%

掌握难度:30分

作用

参考答案

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

实际使用场景

weakmap、weakset、map、set

出现频率: 30%

掌握难度:40分

作用

参考答案

Object:a键只能为字符串或symbol;b强引用;c无序插入;

Map:a键为任意类型;b强引用;c有序插入;

Weakmap:a键只能为对象;b弱引用不可枚举的不容易造成内存泄漏;c有序插入;

Array:a可重复;b有序插入;c强引用;d元素可以是任意值;

Set:a不可重复;b无序插入;c强引用;

WeakSet:a不可重复;b无序插入;c弱引用不可枚举的不容易造成内存泄漏;d元素只能是对象;

实际使用场景

箭头函数有哪些特点

出现频率: 80%

掌握难度:50分

作用

参考答案: 箭头函数的作用就是简化回调函数。

var arr=[1,2,3];
arr.map(function(x){
    console.log(x*x);//1,4,9
})
arr.map(x=>{console.log(x*x)});//1,4,9

更简洁的语法:

  • 外形不同,使用箭头定义,不需要使用function关键字,只能是匿名函数,不能是命名函数;
  • 只有一个形参就不需要用括号括起来
  • 如果函数体只有一行,就不需要放到一个块中
  • 如果 return 语句是函数体内唯一的语句,就不需要 return 关键字

没有自己的this,箭头函数 this 只会从自己的作用域链的上一层继承 this;没有原型对象,不能调用call和apply原型方法;没有supper,不能在继承父类;所以不能通过new调用用作构造函数。

没有arguments,需要用rest代替。

虽然叫做reset参数,但实际上写法是 ...变量名。作用是获取函数的多余参数。比如下面这段代码,要加的数值全都用 ...values代替了。而values的数量可以是任意数量。

function add(...values){
    let sum=0;
    for(let val of values){
        sum=sum+val;
    }
    console.log(sum);
}
add(1,2,3);//6
var object1=(head,...tail)=>{
    console.log([head,tail]);
}
object1(1,"one","two","three");//[1, Array(3)]

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

函数练习题:

var b = 10;
(function b() {
  b = 20;
  console.log(b)
})()

运行结果:

function b() { b = 20; console.log(b) }

分析:

JavaScript 解释器遇到非匿名立即执行函数(题目中的 b)时,会创建一个辅助的特定对象,然后将函数名称当作这个对象的属性,因此函数内部可以访问到 b,但是这个值又是只读的,所以对他的赋值并不生效,所以打印的结果还是这个函数,并且外部的值也没有发生更改。

实际使用场景

函数柯里化

出现频率: 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)

实际使用场景

纯函数

出现频率: 10%

掌握难度:10分

作用

参考答案

对于相同的输入,永远得到相同的输出

没有任何可观察到的副作用

实际使用场景

apply call bind 区别,手写实现原理

出现频率: 50%

掌握难度:80分

作用

参考答案

实际使用场景

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

call:

  // 在原型上添加mycall的方法
Function.prototype.mycall = function(thisArg,...args){
  // 1.获取需要被执行得函数
  var fn = this
  
  // 2.对thisArg转成对象类型(防止它传入得是非对象类型)
  thisArg = (thisArg !== null && thisArg !== undefined) ? Object(thisArg) : window

  // 3.调用需要被执行得函数
  thisArg.fn = fn
  // 若函数有返回值,需进行返回
  var result = thisArg.fn(...args)
  
  //删除函数,防止在thisArg中多出一个函数,难以理解
  delete thisArg.fn
  // 4.将最终得结果返回出去
  return result
}

function foo(){
  console.log("foo函数被执行",this);
}

function sum(num1,num2){
  console.log("sum函数被执行",this,num1,num2);
  return num1 + num2
}

// 系统的函数call方法
// foo.call(undefined)
foo.call({name:'zs'})
foo.call(undefined)
// var result = sum.call({},20,30)
// console.log("系统调用的结果",result);

// 自己实现额函数mycall方法
// foo.mycall({name:"xs"})
// foo.mycall(undefined)

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))
    }
  } 
}

js 的闭包

出现频率: 60%

掌握难度:50分

作用

参考答案

什么是作业域?

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

什么是作用域链?

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

闭包产生的本质

juejin.cn/post/726418…

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

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

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

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

实际使用场景

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

闭包的用处:

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

一般如何产生闭包

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

闭包的应用场景

  • 柯里化 bind
  • 模块

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

new操作符实现原理

出现频率:30%

掌握难度:30分

作用

参考答案

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

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

实际使用场景

类的继承

出现频率: 70%

掌握难度:80分

作用

参考答案

原型继承

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

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
  }
}

参照文章:juejin.cn/post/684490…

实际使用场景

ES6 的 class 和普通构造函数

出现频率: 70%

掌握难度:70分

作用

参考答案

练习题:

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 来判断调用方式

实际使用场景

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

出现频率: 50%

掌握难度:70分

作用

参考答案

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

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

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

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

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

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

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

node 中的事件循环机制

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

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

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

  • 宏任务:scriptsetTimeOutsetIntervalsetImmediate

  • 微任务:promise.then,process.nextTickObject.observeMutationObserver

  • 注意:Promise是同步任务

作者:独立开发者刘学生
链接:juejin.cn/post/729666…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

实际使用场景

事件委托以及冒泡原理

出现频率: 50%

掌握难度:30分

作用

参考答案

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

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

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

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

实际使用场景

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

出现频率: 30%

掌握难度:10分

作用

参考答案

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

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

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

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

实际使用场景

拖拽会用到哪些事件

出现频率: 30%

掌握难度:10分

作用

参考答案

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

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

实际使用场景

promise

出现频率: 80%

掌握难度:60分

作用

参考答案

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

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

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

说说 Promise 的原理?你是如何理解 Promise 的?

做到会写简易版的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))
        }
      })
    }

实际使用场景

ajax、axios、fetch 的区别

出现频率: 50%

掌握难度:50分

作用

参考答案

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 规范。

实际使用场景

js的异步处理函数

出现频率: 20%

掌握难度:50分

作用

参考答案

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

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

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

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

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

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

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

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

参考文章:juejin.cn/post/710836…

实际使用场景

跨域

出现频率: 30%

掌握难度:80分

作用

参考答案

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 元素接收到响应中的脚本代码后,就会自动的执行它们;

实际使用场景

ES6 新增哪些东西

出现频率: 50%

掌握难度:50分

作用

参考答案

支持模块化(import、export);

let、const 关键字;

箭头函数;

类(class、constructor、extends);

Promise、weakmap、weakset、map、set、SymbolproxyReflect

新增一些数组、字符串等内置构造函数方法,例如 Array.fromArray.of 、Math.signMath.trunc 等

扩展操作符、解构、函数默认参数、字符串模板等

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

blog.csdn.net/jiangjunyua…

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

扩展运算符使用场景:

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

练习题:

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

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

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

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

实际使用场景

this 指向

出现频率: 80%

掌握难度:50分

作用

参考答案

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

  • 在函数体中,非显式或隐式地简单调用函数时,在严格模式下,函数内的 this 会被绑定到 undefined 上,在非严格模式下则会被绑定到全局对象 window/global 上。

  • 一般通过上下文对象调用函数时,函数体内的 this 会被绑定到该对象上。

  • 一般使用 new 方法调用构造函数时,构造函数内的 this 会被绑定到新创建的对象上。

  • 一般通过 call/apply/bind 方法显式调用函数时,函数体内的 this 会被绑定到指定参数的对象上。

  • 在箭头函数中,this 的指向是由外层(函数或全局)作用域来决定的。

实际使用场景

continue 和 break 的区别

出现频率: 30%

掌握难度:10分

作用

参考答案

break:用于永久终止循环。即不执行本次循环中 break 后面的语句,直接跳出循环。

continue:用于终止本次循环。即本次循环中 continue 后面的代码不执行,进行下一次循环的入口判断。

实际使用场景

Generator是怎么样使用的以及各个阶段的变化如何?

出现频率: 10%

掌握难度:50分

作用

参考答案

普通函数是一次性把函数内的所有代码同步执行完毕,而生成器函数打破了这种代码执行顺序,而以一种看似同步的的异步方式来执行的代码。生成器函数用来返回迭代器对象,通过调用迭代器对象的next()方法来执行函数内的yield关键字的每一次暂停,并返回一个对象{done:false/true,value:value}。像数组字符串等数据结构都内置了迭代器所以可以直接遍历,但是对象没有内置迭代器,不能使用for/of遍历。如果想用for/of 遍历对象需要在对象内自定义迭代器。

首先生成器是一个函数,用来返回迭代器的

调用生成器后不会立即执行,而是通过返回的迭代器来控制这个生成器的一步一步执行的

通过调用迭代器的next方法来请求一个一个的值,返回的对象有两个属性,一个是value,也就是值;另一个是done,是个布尔类型,donetrue说明生成器函数执行完毕,没有可返回的值了,

donetrue后继续调用迭代器的next方法,返回值的value为undefined

状态变化:

每当执行到yield属性的时候,都会返回一个对象
这时候生成器处于一个非阻塞的挂起状态
调用迭代器的next方法的时候,生成器又从挂起状态改为执行状态,继续上一次的执行位置执行
直到遇到下一次yield依次循环
直到代码没有yield了,就会返回一个结果对象done为true,value为undefined

实际使用场景

作用域链、原型链

出现频率

掌握难度

作用

参考答案

什么是作用域链?

juejin.cn/post/729702…

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

什么原型链?

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

实际使用场景

defer 与 async 的区别

出现频率: 10%

掌握难度:20分

作用

参考答案

juejin.cn/post/689462…

Web 应用程序一般都全部将 JavaScript 引用放在 body 元素中页面的内容后面

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

defer (延迟脚本)

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

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

async(异步脚本)

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

实际使用场景

js 模块化

出现频率: 20%

掌握难度:50分

作用

参考答案

起初js逻辑简单、代码量少,通过script标签引入文件;

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

后来js逻辑复杂、代码量多,再通过script标签引入文件就会出现命名冲突、代码混乱等问题;

所以出现了各种模块化规范来解决命名冲突、代码混乱等问题;

CommonJS 规范:

主要用在 node 开发上; 每个文件就是一个模块,每个文件都有自己的一个作用域; 通过module.exports 暴露 public 成员,通过 require 引入模块依赖,require 函数可以引入 node 的内置模块、自定义模块和 npm 等第三方模块;

优点:

  1. 简单并且容易使用
  2. 服务器端模块便于重用

缺点:

  1. 同步的模块加载方式不适合在浏览器环境中
  2. 不能非阻塞的并行加载多个模块

AMD 规范:

它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

**定义模块:**
define(id?, dependencies?, factory);
**加载模块:**
require([module], callback);

优点:

  1. 适合在浏览器环境中异步加载模块
  2. 可以并行加载多个模块

缺点:

  1. 提高了开发成本
  2. 不符合通用的模块化思维方式

UMD 规范:

UMD 是 (Universal Module Definition) 通用模块定义 的缩写。UMDAMDCommonJS 的一个糅合。AMD 是浏览器优先,异步加载;CommonJS 是服务器优先,同步加载。

既然要通用,怎么办呢?那就先判断是否支持 node 的模块,支持就使用 node;再判断是否支持 AMD,支持则使用 AMD 的方式加载。这就是所谓的 UMD

CMD 规范:

CMD 是 (Common Module Definition) 公共模块定义 的缩写。CMD 可能是在 CommonJS 之后抽象出来的一套模块化语法定义和使用的标准。

CMD 规范中,一个模块就是一个文件。

define(factory);
seajs.use([module], callback);

优点:可以很容易在 node 中运行

缺点:依赖 SPM 打包,模块的加载逻辑偏重

ES6 模块化:

ES6 模块的设计思想是尽量的 静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJSAMD 模块,都只能在运行时确定这些东西。

ES6 中,我们使用 export 关键字来导出模块,使用 import 关键字来引入模块。

import { stat, exists, readFile } from 'fs';
export { firstName, lastName, year };

优点:容易进行静态分析

缺点:原生浏览器端还没有实现该标准

AMD 和 CMD 的区别

  1. 对于依赖的模块,AMD提前执行CMD延迟执行
  2. AMD 推崇 依赖前置CMD 推崇 依赖就近
  3. AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。

ES6 模块与 CommonJS 模块的差异

  1. CommonJS 模块输出的是一个 值的拷贝ES6 模块输出的是 值的引用
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  3. CommonJS 模块的 require()同步加载 模块,ES6 模块的 import 命令是 异步加载,有一个独立的模块依赖的解析阶段。

参考文章:

juejin.cn/post/720396…

实际使用场景

内存泄漏

出现频率: 50%

掌握难度:60分

作用

参考答案

不再使用的内存系统无法回收,导致没有实际用途的内存无法释放,浪费内存,影响性能。

回收内存方法:

标记-清除法;

引用计数法:

存在循环引用的问题。

避免内存泄漏的方式:

把无用对象赋值为null;

清楚无用的定时器;

将不再使用的引用dom的变量设置为null;从内到外执行 appendChild:

移除不再使用的事件绑定;

将不再使用的闭包变量设置为null;

实际使用场景

说说严格模式的限制

出现频率: 10%

掌握难度:50分

作用

参考答案

什么是严格模式?

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

例如:

进入严格模式后的限制

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

实际使用场景

常见兼容性问题

出现频率: 50%

掌握难度:70分

作用

参考答案

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

  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

实际使用场景

错误处理机制

出现频率: 30%

掌握难度:50分

作用

参考答案

下面代码的输出是什么?( 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

实际使用场景

如何编写高性能的 JavaScript

出现频率: 50%

掌握难度:80分

作用

参考答案

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

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

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

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

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

  • 尽量减少使用闭包

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

  • 尽量减少对象成员嵌套

  • 缓存 DOM 节点的访问

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

  • setTimeout()setInterval() 传递函数而不是字符串作为参数

  • 尽量使用直接量创建对象和数组

  • 最小化重绘 (repaint) 和回流 (reflow)


  • 垃圾回收
  • 闭包中的对象清楚
  • 防抖节流
  • 分批加载(setInterval,加载10000个节点)
  • 事件委托
  • 少用with
  • requestAnimationFrame的使用
  • script标签中的defer和async
  • CDN

实际使用场景

防抖和节流

出现频率:50%

掌握难度:50分

作用

参考答案

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

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

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

  • 防抖

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

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

具体实现:

/**
 * 函数防抖
 * @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;
        }
    }
}

实际使用场景

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

出现频率: 20%

掌握难度:80分

作用

参考答案

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

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

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

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

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

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

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

实际使用场景

链式操作的实现方式

出现频率: 10%

掌握难度:10分

作用

参考答案

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

实际使用场景

增、删、改、查DOM节点

出现频率: 40%

掌握难度:40分

作用

参考答案

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

实际使用场景

document.write 和 innerHTML 的区别

出现频率: 10%

掌握难度:10分

作用

参考答案

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

实际使用场景

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

出现频率: 10%

掌握难度:10分

作用

参考答案

页面加载完成有两种事件:

一是 ready,表示文档结构已经加载完成(不包含图片等非文字媒体文件);

二是 onload,指示页面包含图片等文件在内的所有元素都加载完成。

实际使用场景

clientWidth,offsetWidth,scrollWidth 的区别

出现频率: 10%

掌握难度:50分

作用

参考答案

clientWidth = width+左右 padding

offsetWidth = width + 左右 padding + 左右 boder

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

实际使用场景

localStorage、SessionStorage、cookie、session 之间有什么区别

出现频率: 50%

掌握难度:30分

作用

参考答案

实际使用场景

localStorage

生命周期:关闭浏览器后数据依然保留,除非手动清除,否则一直在
作用域:相同浏览器的不同标签在同源情况下可以共享localStorage

sessionStorage

生命周期:关闭浏览器或者标签后即失效
作用域:只在当前标签可用,当前标签的iframe中且同源可以共享

cookie

是保存在客户端的,一般由后端设置值,可以设置过期时间
储存大小只有4K
一般用来保存用户的信息的
在http下cookie是明文传输的,较不安全
cookie属性有
    http-only:不能被客户端更改访问,防止XSS攻击(保证cookie安全性的操作)
    Secure:只允许在https下传输
    Max-age: cookie生成后失效的秒数
    expire: cookie的最长有效时间,若不设置则cookie生命期与会话期相同

session

session是保存在服务端的
session的运行依赖sessionId,而sessionId又保存在cookie中,所以如果禁用的cookie,session也是不能用的,不过硬要用也可以,可以把sessionId保存在URL中
session一般用来跟踪用户的状态
session 的安全性更高,保存在服务端,不过一般为使服务端性能更加,会考虑部分信息保存在cookie中

localstorage存满了怎么办?

  • 划分域名,各域名下的存储空间由各业务组统一规划使用
  • 跨页面传数据:考虑单页应用、采用url传输数据
  • 最后兜底方案:情调别人的存储

怎么使用cookie保存用户信息⭐⭐⭐

  • document.cookie(“名字 = 数据;expire=时间”)

怎么删除cookie⭐⭐⭐

  • 目前没有提供删除的操作,但是可以把它的Max-age设置为0,也就是立马失效,也就是删除了

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

出现频率: 10%

掌握难度:50分

作用

参考答案

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

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

实际使用场景

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

出现频率: 10%

掌握难度:50分

作用

参考答案

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

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

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

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

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

Lambda 架构

实际使用场景

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

出现频率: 10%

掌握难度:80分

作用

参考答案

编程范式 Programming paradigm 是指计算机中编程的典范模式或方法。

常见的编程范式有:函数式编程、程序编程、面向对象编程、指令式编程、面向切面编程(juejin.cn/post/715904…

不同的编程语言也会提倡不同的“编程范型”。一些语言是专门为某个特定的范型设计的,如 Smalltalk 和 Java 支持面向对象编程。而 Haskell 和 Scheme 则支持函数式编程。现代编程语言的发展趋势是支持多种范型,例如 ES 支持函数式编程的同时也支持面向对象编程

实际使用场景

js常见的设计模式

出现频率: 20%

掌握难度:80分

作用

参考答案

  • 单例模式、工厂模式、构造函数模式、发布订阅者模式、迭代器模式、代理模式

  • 单例模式

    • 不管创建多少个对象都只有一个实例
    var Single = (function () {
      var instance = null
      function Single(name) {
        this.name = name
      }
      return function (name) {
        if (!instance) {
          instance = new Single(name)
        }
        return instance
      }
    })()

    var oA = new Single('hi')
    var oB = new Single('hello')
    console.log(oA);
    console.log(oB);
    console.log(oB === oA);

工厂模式

  • 代替new创建一个对象,且这个对象想工厂制作一样,批量制作属性相同的实例对象(指向不同)
    function Animal(o) {
      var instance = new Object()
      instance.name = o.name
      instance.age = o.age
      instance.getAnimal = function () {
        return "name:" + instance.name + " age:" + instance.age
      }
      return instance
    }

    var cat = Animal({name:"cat", age:3})
    console.log(cat);
  • 构造函数模式

  • 发布订阅者模式

    class Watcher {
      // name模拟使用属性的地方
      constructor(name, cb) {
        this.name = name
        this.cb = cb
      }
      update() {//更新
        console.log(this.name + "更新了");
        this.cb() //做出更新回调
      }
    }

    class Dep {//依赖收集器
      constructor() {
        this.subs = []
      }
      addSubs(watcher) {
        this.subs.push(watcher)
      }
      notify() {//通知每一个观察者做出更新
        this.subs.forEach(w => {
          w.update()
        });
      }
    }

    // 假如现在用到age的有三个地方
    var w1 = new Watcher("我{{age}}了", () => { console.log("更新age"); })
    var w2 = new Watcher("v-model:age", () => { console.log("更新age"); })
    var w3 = new Watcher("I am {{age}} years old", () => { console.log("更新age"); })

    var dep = new Dep()
    dep.addSubs(w1)
    dep.addSubs(w2)
    dep.addSubs(w3)


    // 在Object.defineProperty 中的 set中运行
    dep.notify()
  • 代理模式

  • 迭代器模式

实际使用场景

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

出现频率:10%

掌握难度:70分

作用

参考答案

关于第一个问题,这不是三言两语或者几行文字就能够讲清楚的,这里放上一篇博文地址: segmentfault.com/a/119000001…

第二个问题:解释型语言和编译型语言的差异是什么?

电脑能认得的是二进制数,不能够识别高级语言。所有高级语言在电脑上执行都需要先转变为机器语言。但是高级语言有两种类型:编译型语言和解释型语言。常见的编译型语言语言有C/C++、Pascal/Object 等等。常见的解释性语言有python、JavaScript等等。

编译型语言先要进行编译,然后转为特定的可执行文件,这个可执行文件是针对平台的(CPU类型),可以这么理解你在PC上编译一个C源文件,需要经过预处理,编译,汇编等等过程生成一个可执行的二进制文件。当你需要再次运行改代码时,不需要重新编译代码,只需要运行该可执行的二进制文件。优点,编译一次,永久执行。还有一个优点是,你不需要提供你的源代码,你只需要发布你的可执行文件就可以为客户提供服务,从而保证了你的源代码的安全性。但是,如果你的代码需要迁移到linux、ARM下时,这时你的可执行文件就不起作用了,需要根据新的平台编译出一个可执行的文件。这也就是多个平台需要软件的多个版本。缺点是,跨平台能力差。

解释型语言需要一个解释器,在源代码执行的时候被解释器翻译为一个与平台无关的中间代码,解释器会把这些代码翻译为及其语言。打个比方,编译型中的编译相当于一个翻译官,它只能翻译英语,而且中文文章翻译一次就不需要重新对文章进行二次翻译了,但是如果需要叫这个翻译官翻译德语就不行了。而解释型语言中的解释器相当于一个会各种语言的机器人,而且这个机器人回一句一句的翻译你的语句。对于不同的国家,翻译成不同的语言,所以,你只需要带着这个机器人就可以。解释型语言的有点是,跨平台,缺点是运行时需要源代码,知识产权保护性差,运行效率低。

实际使用场景

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

出现频率: 10%

掌握难度:30分

作用

参考答案

blog.csdn.net/zhangjin501…

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 个字节来存储,长度既固定又可变。

实际使用场景

escape、encodeURI、encodeURIComponent 的区别

出现频率: 10%

掌握难度:50分

作用

参考答案

在使用url进行参数传递时,经常会传递一些中文名(或含有特殊字符)的参数或URL地址,在后台处理时会发生转换错误。在有些传递页面使用GB2312,而在接收页面使用UTF8,这样接收到的参数就可能会与原来发生不一致。使用服务器端的urlEncode函数编码的URL,与使用客户端javascript的encodeURI函数编码的URL,结果就不一样。javascript对文字进行编码涉及3个函数:escape,encodeURI,encodeURIComponent,相应3个解码函数:unescape,decodeURI,decodeURIComponent

blog.csdn.net/weixin_4473…

blog.csdn.net/winy_lm/art…

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

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

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

实际使用场景

缓存

出现频率

掌握难度

作用

参考答案

cookie:juejin.cn/post/706583…

实际使用场景

****

出现频率

掌握难度

作用

参考答案

实际使用场景

****

出现频率

掌握难度

作用

参考答案

实际使用场景

****

出现频率

掌握难度

作用

参考答案

实际使用场景

****

出现频率

掌握难度

作用

参考答案

实际使用场景

****

出现频率

掌握难度

作用

参考答案

实际使用场景

****

出现频率

掌握难度

作用

参考答案

实际使用场景

****

出现频率

掌握难度

作用

参考答案

实际使用场景

****

出现频率

掌握难度

作用

参考答案

实际使用场景