前端面试题--js

文章目录

  • 1. es6 有哪些新特性?
  • 2. let const var 相关
  • 3. 暂时性死区
  • 4. js数据类型,区别
  • 5. js 判断数据类型的方法
  • 7. Object.assign的理解
  • 8. map 和 forEach 的区别
  • 9.for of 可以遍历哪些对象
  • 10. iframe有什么优点、缺点
  • 11. 变量提升
  • 12. 作用域
  • 13. HashMap 和 ArrayMap 有什么区别
  • 14. JS 原型和原型链
    • 1. 原型和原型链的基础结论
  • 15. 数组方法
  • 16. 数组有哪几种循环方式?分别有什么作用?
  • 17. 数组去重
      • 一、利用ES6 Set去重(ES6中最常用)
      • 二、利用for嵌套for,然后splice去重(ES5中最常用)
      • 三、利用indexOf去重
      • 四、利用includes
      • 五、利用hasOwnProperty
  • 18.null 和 undefined 的区别,如何让一个属性变为null
  • 19.数组和伪数组的区别
  • 20. 介绍下 Set、Map、WeakSet 和 WeakMap 的区别?
  • 21. 简单说说 js 中有哪几种内存泄露的情况
  • 22. 同步和异步
  • 23. 宏任务和微任务
  • 24. promise和 async await 区别
  • 25. 用js实现sleep,用promise
  • 26. 循环i,setTimeout 中输出什么,如何解决(块级作用域,函数作用域)
  • 27. this 的指向有哪些
  • 28. 常用的字符串方法有哪些?
  • 29. 什么是闭包?手写一个闭包函数? 闭包有哪些优缺点?
  • 30. js 继承
  • 31. 用什么实现跨域
  • 32. cookie,localstorage, sessionstorage
  • 33. Cookie 和 Session的关系和区别
  • 34. js 的执行机制是怎么样的?
  • 35. 什么是深拷贝、什么是浅拷贝?以及实现方式
  • 36. 请写出至少两种常见的数组排序的方法(原生 js)
  • 37. es6中箭头函数
  • 38. 箭头函数有哪些特征,请简单描述一下它?
  • 39. call、apply、bind 三者的异同
  • 40. constructor的理解
  • 41. new会发生什么
  • 42. 为什么js是单线程
  • 43. 死锁
  • 44. 面向对象的三个特征,分别说一下什么意思
  • 45. 防抖和节流的原理和使用场景
  • 46. 获取当前页面url
  • 47. js中两个数组怎么取交集+(差集、并集、补集)
  • 48. 写一个判断是否是空对象的函数
  • 49. 字符串中的单词逆序输出(手写)
  • 50. 给定一个字符串,请你找出其中不含有重复字符的最长子串的长度
  • 51. 判断输出console.log([] == []) console.log([] == 0)
  • 52. 三数之和
  • 53. 浏览器如何渲染页面的?
  • 54. 重绘、重排区别如何避免
  • 55. 事件循环Event loop
  • 56. 浏览器垃圾回收机制
  • 57. 顺序存储结构和链式存储结构的比较
  • 58. token 能放在cookie中吗
  • 59. 前端性能优化手段
  • 60. ajax
  • 61. webpack 的 配置
      • webpack的loader和plugin有什么区别
      • 二者区别:
  • 62. git
  • 63. 在分支a工作代码还没修改完,这时突然有一个紧急的bug需要你去b分支修复,但是a分支的工作还不能add,应该用什么git指令(git stash)
  • 64. es6 中 extends 类继承实现原理
  • 65. js往原型中添加方法
  • 66. 有哪些异步处理方法?
  • 67. async await和promise的区别?
  • 68. 登录逻辑
  • 69. 为什么typeof null 结果是object
  • 70. 事件冒泡和事件捕获
  • 71. DOM事件流的三个阶段
  • 72. 实现一个类(es5与es6)手写
  • 72. 后台管理系统中的权限管理是怎么实现的?
  • 73. 知道lodash吗?它有哪些常见的API ?
  • 74. 优化上传很大的文件
        • 分片上传
        • 断点续传
          • 二、实现思路
          • 三、使用场景
  • 75. localstorage 的跨域存储方案
  • 76. 怎么删除、添加、获取localstorage里面的数据
  • 77. for in 循环会遍历原型链上的属性的问题
  • 78. settimeout延时3秒所经历的时间一定是3秒吗?
  • 79. 为什么设计时要区分宏任务、微任务
  • 80. 对nodejs里的comonjs的理解?对模块化规范的理解?
  • 81. 哪些方法可以判断是一个数组 typeof 数组 返回什么
      • 1.通过instanceof运算符判断
      • 2.通过constructor判断
      • 3.通过数组自带的isArray方法判断
      • 4.通过isPrototypeOf()方法判断
      • 5.通过Object.getPrototypeOf方法判断
      • 6.通过Object.prototype.toString.call()判断
  • 32. 什么是预加载

1. es6 有哪些新特性?

ES6 是 2015 年推出的一个新的版本、这个版本相对于 ES5 的语法做了很多的优化、例如:

  1. 新增了let、 const, let 和 const具有块级作用域,不存在变量提升的问题

  2. 新增了箭头函数,简化了定义函数的写法,同时 可以巧用箭头函数的 this、(注意箭头函数本身没有 this,它的 this 取决于外部的环境)

  3. 新增了promise, 解决了回调地狱的问题

  4. 新增了模块化、利用 import 、export 来实现导入、导出。

  5. 新增了解构赋值, ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构 (Destructuring)。

  6. 新增了class 类的概念,它类似于对象。

  7. 扩展运算符…

    2.6 Object 和 Symbol

    (1) Object对象

    支持简写:同名属性K-V可以省略一个、函数声明可以省略function;支持属性名表达式、函数名表达式。(注意:以上2个——表达式和简写不能同时使用)。

    对象的方法的name属性返回方法名,但有几个例外情况要小心。新增了Object方法

    Object.is()——用于解决 == 和 === 的部分兼容问题

    Object.assign()——将src的所有可枚举对象属性复制到dest对象上(浅复制)

    Object.setPrototypeOf()、Object.getPrototypeOf() (Object.__proto属性)

    Object.entries()、Object.keys()、Object.values()

    ES6中5种遍历对象属性的方法

    for-in——自身和继承的可枚举属性(除Symbol)

    Object.keys()——自身非继承的可枚举属性(除Symbol)

    Object.getOwnPropertyNames()——自身所有属性键名(包括不可枚举、除Symbol)

    Object.getOwnPropertySymbols()——自身的所有 Symbol 属性的键名

    Reflect.ownKeys()——自身的所有键名

    (2)Symbol类型

    ES5以前,对象属性都只能是字符串,容易造成重命名导致的冲突。Symbol提供了一种机制,可以保存 属性名是独一无二的。Symbol类型的使用注意:1)创建是调用函数,而不是new关键字 2)Symbol类 型的属性不会被for-*、Object.keys()、Object.getPropertyNames()返回,可以用后面两种方法遍历。

  8. 数据结构Set和Map

    Set是一种类似数组的数据结构,区别在于其存储的成员都是不重复的,由此带来了它的一个应用就是:去重。Set通过new关键字实例化,入参可以是数组or类数组的对象。

    值得注意的是:在Set中,只能存储一个NaN,这说明在Set数据结构中,NaN等于NaN

    Set实例的方法:操作方法add()、delete()、has()和clear();遍历方法:keys()、values()、entries()和forEach();扩展运算符…、数组方法map()、filter()方法也可以用于Set结构。由此它可以很方便的实现数组的交、并、差集。

    WeakSet类似于Set,主要区别在于1.成员只能是对象类型;2.对象都是弱引用(如果其他对象都不再引用该对象,垃圾回收机制会自动回收该对象所占的内存,不可预测何时会发生,故WeakSet不可被遍历)

    JavaScript对象Object都是键值K-V对的集合,但K取值只能是字符串和Symbol,Map也是K-V的集合,然而其K可以取任意类型。如果需要键值对的集合,Map比Object更适合。Map通过new关键字实例化。

    Map实例的方法:set()、get()、has()、delete()和clear();遍历方法同Set。

    Map与其它数据结构的互相转换:Map <—> 数组| Map <—> 对象| Map <—> JSON。

    WeakMap类似于Map,主要区别在于:1.只接受对象作为键名;2.键名所指向的对象不计入垃圾回收机制

  9. Iterator

    ES6之前在JS中只有Array和对象可以表示“集合”这种数据结构,ES6中增加了:Set和Map。由此,四种之间互相组合又可以定义新的数据结构。这些新定义的数据结构如何访问呢?遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。遍历器对象本质上是一个指针对象。

    只要为某个数据结构部署 了Iterator接口,则可以称此数据结构是可遍历的。iterator属性部署在Symbol上。如下对象默认部署了Iterator结口:Array Set Map String等。部署iterator结构的要点:1)在Symbol.iterator上部署;2)必须包含next()函数。默认调用iterator接口的场景:解构赋值、…扩展运算符、yeild* 。for-of循环内部调用的即是调用数据机构内部的Symbol.iterator方法。

    for-in和for-of循环

    for-in用于遍历对象属性,对象自身和继承的可枚举属性(不可遍历Symbol属性)

    for-of循环是一种遍历所有数据机构的统一方法。实现原理是数据结构上部署的Symbol.iterator属性。

2. let const var 相关

  • var ——ES5 变量声明方式

    在变量未赋值时,变量undefined(为使用声明变量时也为undefined)

    作用域——var的作用域为方法作用域;只要在方法内定义了,整个方法内的定义变量后的代码都可以使用

  • let——ES6变量声明方式

    在变量为声明前直接使用会报错

    作用域——let为块作用域——通常let比var 范围要小

    let禁止重复声明变量,否则会报错;var可以重复声明

  • const——ES6变量声明方式

    const为常量声明方式;声明变量时必须初始化,在后面出现的代码中不能再修改该常量的值

    const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动

3. 暂时性死区

暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

扩展:

let 、const与暂时性死区

letconst 声明的变量拥有暂时性死区(TDZ):当进入它的作用域,它不能被访问(获取或设置)直到执行到达声明。

首先看看不具有暂时性死区的 var

  • 当进入 var 变量的作用域(包围它的函数),立即为它创建(绑定)存储空间。变量会立即被初始化并赋值为 undefined
  • 当执行到变量声明的时候,如果变量定义了值则会被赋值。

通过 let 声明的变量拥有暂时性死区,生命周期如下:

  • 当进入 let 变量的作用域(包围它的语法块),立即为它创建(绑定)存储空间。此时变量仍是未初始化的。
  • 获取或设置未初始化的变量将抛出异常 ReferenceError
  • 当执行到变量声明的时候,如果变量定义了值则会被赋值。如果没有定义值,则赋值为 undefined

const 工作方式与 let 类似,但是定义的时候必须赋值并且不能改变。

在 TDZ 内部,如果获取或设置变量将抛出异常:

if (true) { // enter new scope, TDZ starts
    // Uninitialized binding for `tmp` is created

    tmp = 'abc'; // ReferenceError
    console.log(tmp); // ReferenceError

    let tmp; // TDZ ends, `tmp` is initialized with `undefined`
    console.log(tmp); // undefined

    tmp = 123;
    console.log(tmp); // 123
}

下面的示例将演示死区(dead zone)是真正短暂的(基于时间)和不受空间条件限制(基于位置)

if (true) { // enter new scope, TDZ starts
    const func = function () {
        console.log(myVar); // OK!
    };

    // Here we are within the TDZ and
    // accessing `myVar` would cause a `ReferenceError`

    let myVar = 3; // TDZ ends
    func(); // called outside TDZ
}

typeof 与暂时性死区

变量在暂时性死区无法被访问,所以无法对它使用 typeof

if (true) {
    console.log(typeof tmp); // ReferenceError
    let tmp;
}

4. js数据类型,区别

前端面试题--js_第1张图片

  • 基本数据类型:

Number,String,Boolean,null,undefined,symbol,bigint(后两个为ES6新增)

  • 引用数据类型:

object,function(proto Function.prototype)

object:普通对象,数组对象,正则对象,日期对象,Math数学函数对象。

  • 两种数据存储方式:
  1. 基本数据类型是直接存储在栈中的简单数据段,占据空间小、大小固定,属于被频繁使用的数据。栈是存储基本类型值和执行代码的空间。

  2. 引用数据类型是存储在堆内存中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址,当解释器寻找引用值时,会检索其在栈中的地址,取得地址后从堆中获得实体。

  • 两种数据类型的区别:
  1. 堆比栈空间大,栈比堆运行速度快。
  2. 堆内存是无序存储,可以根据引用直接获取。
  3. 基础数据类型比较稳定,而且相对来说占用的内存小。
  4. 引用数据类型大小是动态的,而且是无限的。
  5. 其中原始数据类型也称基础数据类型,是不可拆分的数据类型,他存在于栈中;而引用数据类型也是通常意义上所说的,存在于堆中。
    这两者的一个重要的区别在于原始数据类型在赋值的时候使用的是传值的方式,而引用数据类型在赋值时使用的是传址(指针)的方式

5. js 判断数据类型的方法

然后判断数据类型的方法一般可以通过:typeof、instanceof、constructor、toString四种常用方法

不同类型的优缺点 typeof instanceof constructor Object.prototype.toString.call
优点 使用简单 能检测出引用 类型 基本能检测所有 的类型(除了 null 和 undefined) constructor 易被 修改,也不能跨 iframe
缺点 只能检测 出基本类 型(出 null) 不能检测出基 本类型,且不 能跨 iframe constructor 易被 修改,也不能跨 iframe IE6 下,undefined 和 null 均为 Object

7. Object.assign的理解

作用:Object.assign 可以实现对象的合并。

语法:Object.assign(target, ...sources)

解析

  1. Object.assign 会将 source 里面的可枚举属性复制到 target,如果和 target 的已有属性重名,则会覆盖。
  2. 后续的 source 会覆盖前面的 source 的同名属性。
  3. Object.assign 复制的是属性值,如果属性值是一个引用类型,那么复制的其实是引用地址,就会存在引用共享的问题。

https://www.jianshu.com/p/f9ec860ecd81

8. map 和 forEach 的区别

示例
下方提供了一个数组,如果我们想将其中的每一个元素翻倍,我们可以使用mapforEach来达到目的。

let arr = [1, 2, 3, 4, 5];

ForEach
注意,forEach是不会返回有意义的值的。

我们在回调函数中直接修改arr的值。

arr.forEach((num, index) => {    
    return arr[index] = num * 2;}
);

执行结果如下:

// arr = [2, 4, 6, 8, 10]

Map

let doubled = arr.map(num => {
    return num * 2;
});

执行结果如下:

// doubled = [2, 4, 6, 8, 10]
  • 相同点:
  1. 都是循环遍历数组中的每一项
  2. 每次执行匿名函数都支持三个参数,参数分别为 item(当前每一项),index(索引值),arr(原数组)
  3. 匿名函数中的 this 都是指向 window
  4. 只能遍历数组
  • 不同点:
  1. map() 会分配内存空间存储新数组返回,forEach() 不会返回数据,没有返回值,即返回是 undefined。
  2. forEach() 允许 callback 更改原始数组的元素。map() 返回新的数组
  3. 因为 map & forEach 的主要区别是有无返回,所以,当你想基于一个原数组返回一个新数组,可以选择 map,当你只是想遍历数据不需要考虑返回时可以选择 forEach。

哪个更好呢?
取决于你想要做什么。

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

let arr = ['a', 'b', 'c', 'd'];
arr.forEach((letter) => {
    console.log(letter);
});
// a
// b
// c
// d

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

let arr = [1, 2, 3, 4, 5];
let arr2 = arr.map(num => num * 2).filter(num => num > 5);
// arr2 = [6, 8, 10]

我们首先使用map将每一个元素乘以2,然后紧接着筛选出那些大于5的元素。最终结果赋值给arr2。

核心要点

  • 能用forEach()做到的,map()同样可以。反过来也是如此。
  • map()会分配内存空间存储新数组并返回,forEach()不会返回数据。
  • forEach()允许callback更改原始数组的元素。map()返回新的数组。

9.for of 可以遍历哪些对象

for…of…: 它是es6新增的一个遍历方法,但只限于迭代器(iterator), 所以普通的对象用for…of遍历是会报错的。

可迭代的对象:包括Array, Map, Set, String, TypedArray, 函数的arguments对象,NodeList 对象

10. iframe有什么优点、缺点

优点:

  • iframe能够原封不动的把嵌入的网页展现出来。
  • 如果有多个网页引用iframe,那么你只需要修改iframe的内容,就可以实现调用的每一个页面内容的更改,方便快捷。
  • 网页如果为了统一风格,头部和版本都是一样的,就可以写成一个页面,用iframe来嵌套,可以增加代码的可重用。
  • 如果遇到加载缓慢的第三方内容如图标和广告,这些问题可以由iframe来解决。

缺点:

  • iframe会阻塞主页面的onload事件;
  • iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。会产生很多页面,不容易管理。
  • iframe框架结构有时会让人感到迷惑,如果框架个数多的话,可能会出现上下、左右滚动条,会分散访问者的注意力,用户体验度差。
  • 代码复杂,无法被一些搜索引擎索引到,这一点很关键,现在的搜索引擎爬虫还不能很好的处理iframe中的内容,所以使用iframe会不利于搜索引擎优化(SEO)。
  • 很多的移动设备无法完全显示框架,设备兼容性差。
  • iframe框架页面会增加服务器的http请求,对于大型网站是不可取的。

11. 变量提升

JavaScript是单线程语言,所以执行肯定是按顺序执行。但是并不是逐行的分析和执行,而是一段一段地分析执行,会先进行编译阶段然后才是执行阶段。在编译阶段阶段,代码真正执行前的几毫秒,会检测到所有的变量和函数声明,所有这些函数和变量声明都被添加到名为Lexical Environment的JavaScript数据结构内的内存中。所以这些变量和函数能在它们真正被声明之前使用。

用var声名的变量在声明之前的地方就能被使用的现象称为变量提升,值为undefined,es6新增的let和const解决了变量提升的这一问题,在let和const声明之前会存在暂时性死区,如果在声明之前使用会直接报错。

12. 作用域

概念: 作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。

ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了‘块级作用域’,可通过新增命令 let 和 const 来体现。

扩展:

var ——ES5 变量声明方式

  1. 在变量未赋值时,变量undefined(为使用声明变量时也为undefined)
  2. 作用域——var的作用域为方法作用域;只要在方法内定义了,整个方法内的定义变量后的代码都可以使用

let——ES6变量声明方式

  1. 在变量为声明前直接使用会报错
  2. 作用域——let为块作用域——通常let比var 范围要小
  3. let禁止重复声明变量,否则会报错;var可以重复声明

const——ES6变量声明方式

const为常量声明方式;声明变量时必须初始化,在后面出现的代码中不能再修改该常量的值

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动

13. HashMap 和 ArrayMap 有什么区别

  1. 查找效率
    HashMap 因为其根据 hashcode 的值直接算出 index,所以其查找效率是随着数组长度增大而增加的。
    ArrayMap 使用的是二分法查找,所以当数组长度每增加一倍时,就需要多进行一次判断,效率下降
  2. 扩容数量
    HashMap 初始值16个长度,每次扩容的时候,直接申请双倍的数组空间。
    ArrayMap 每次扩容的时候,如果size长度大于8时申请 size*1.5 个长度,大于 4 小于 8 时申请 8 个,小于 4时申请 4 个。这样比较 ArrayMap 其实是申请了更少的内存空间,但是扩容的频率会更高。因此,如果数据量比较大的时候,还是使用 HashMap 更合适,因为其扩容的次数要比 ArrayMap 少很多。
  3. 扩容效率
    HashMap 每次扩容的时候重新计算每个数组成员的位置,然后放到新的位置。
    ArrayMap 则是直接使用 System.arraycopy,所以效率上肯定是 ArrayMap 更占优势。
  4. 内存消耗
    以 ArrayMap 采用了一种独特的方式,能够重复的利用因为数据扩容而遗留下来的数组空间,方便下一个ArrayMap 的使用。而 HashMap 没有这种设计。 由于ArrayMap 只缓存了长度是4和8的时候,所以如果频繁的使用到Map,而且数据量都比较小的时候,ArrayMap无疑是相当节省内存的。

总结

综上所述,数据量比较小,并且需要频繁的使用Map存储数据的时候,推荐使用ArrayMap。 而数据量比较大的 时候,则推荐使用HashMap。

14. JS 原型和原型链

1. 原型和原型链的基础结论

函数和对象的关系

  • 函数是对象,对象都是通过函数创建的

  • 函数与对象并不是简单的包含与被包含的关系

原型的类别

  • 显示原型:prototype,是每个函数 function 独有的属性

  • 隐式原型:_proto_,是每个对象都具有的属性。

原型和原型链

  • 原型:一个函数可以看成一个类,原型是所有类都有的一个属性,原型的作用就是给这个类的一个对象都添加一个统一的方法。

  • 原型链:每个对象都有一个_proto_, 它指向它的 prototype 原型对象;它的 prototype 原型对象又有一个 _proto _,指向它的 prototype 原型对象,就这样层层向上最终找到顶级对象 Object 的 prototype,这个查询路径就是原型链。

前端面试题--js_第2张图片
前端面试题--js_第3张图片
原型链的作用

  • 数据共享、节省内存空间
  • 实现继承

15. 数组方法

  1. concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组
let a = [1,2,3];
let b = [4,6];
console.log(a.concat(b));
  1. find() 方法返回数组中满足提供的测试函数的第一个元素的值。否则返回 undefined
var ages = [4, 12, 16, 20];
 
function checkAdult(age) {
    return age >= document.getElementById("ageToCheck").value;
}
 
function myFunction() {
    document.getElementById("demo").innerHTML = ages.find(checkAdult);
}
  1. findIndex() 方法返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1
var ages = [3, 10, 18, 20];
 
function checkAdult(age) {
    return age >= 18;
}
function myFunction() {
    document.getElementById("demo").innerHTML = ages.findIndex(checkAdult);
  1. includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true, 否则返回 false。
let site = ['runoob', 'google', 'taobao'];
 
site.includes('runoob'); 
// true 
 
site.includes('baidu'); 
// false
  1. indexOf() 方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。 (通常用它判断数组中有没有这个元素)
var fruits = ["Banana", "Orange", "Apple", "Mango"];
var a = fruits.indexOf("Apple");//2
  1. join() 方法将一个数组(或一个类数组对象)的所有元素连接成一个字符串并返回这个字符串。 如果数组只有一个项目,那么将返回该项目而不使用分隔符。
var fruits = ["Banana", "Orange", "Apple", "Mango"];
var energy = fruits.join();//Banana,Orange,Apple,Mango
var fruits = ["Banana", "Orange", "Apple", "Mango"];
var energy = fruits.join(" and ");//Banana and Orange and Apple and Mango
  1. pop() 方法从数组中删除最后一个元素,并返回该元素的值。此方法更改数组的长度。
var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.pop();//Banana,Orange,Apple

8 . push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。

var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.push("Kiwi")//Banana,Orange,Apple,Mango,Kiwi
  1. shift() 方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度
var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.shift()//Orange,Apple,Mango
  1. unshift() 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度(该方法修改原有数组)。
var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.unshift("Lemon","Pineapple");//Lemon,Pineapple,Banana,Orange,Apple,Mango
  1. splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内 容。此方法会改变原数组。 由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删 除元素,则返回空数组。
var fruits = ["Banana", "Orange", "Apple", "Mango"];
//在索引为2位置开始删除0个元素,同时添加"Lemon","Kiwi"
fruits.splice(2,0,"Lemon","Kiwi");//Banana,Orange,Lemon,Kiwi,Apple,Mango
  1. slice() 方法同上,但不会改变原数组

    slice() 方法可从已有的数组中返回选定的元素。

    slice() 方法可提取字符串的某个部分,并以新的字符串返回被提取的部分。

var fruits = ["Banana", "Orange", "Lemon", "Apple", "Mango"];
var citrus = fruits.slice(1,3);//Orange,Lemon
  1. reverse() 方法将数组中元素的位置颠倒,并返回该数组。该方法会改变原数组
var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.reverse();//Mango,Apple,Orange,Banana
  1. sort() 方法用原地算法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字 符串,然后比较它们的 UTF-16 代码单元值序列时构建的,会改变原数组
var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.sort();//Apple,Banana,Mango,Orange

16. 数组有哪几种循环方式?分别有什么作用?

  1. every() 方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。

    every() 方法用于检测数组所有元素是否都符合指定条件(通过函数提供)。

    every() 方法使用指定函数检测数组中的所有元素:

    • 如果数组中检测到有一个元素不满足,则整个表达式返回 false ,且剩余的元素不会再进行检测。
    • 如果所有元素都满足条件,则返回 true。
var ages = [32, 33, 16, 40];

function checkAdult(age) {
    return age >= 18;
}

function myFunction() {
    document.getElementById("demo").innerHTML = ages.every(checkAdult);//false
}
  1. filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。 注意: filter() 不会对空数组进行检测。 注意: filter() 不会改变原始数组。
var ages = [32, 33, 16, 40];

function checkAdult(age) {
    return age >= 18;
}

function myFunction() {
    document.getElementById("demo").innerHTML = ages.filter(checkAdult);//32,33,40
}
  1. forEach() 方法对数组的每个元素执行一次提供的函数。

    前端面试题--js_第4张图片

var numbers = [4, 9, 16, 25];
function a(num,index) {
    console.log(num);
    console.log("---------------");
    console.log(index);
}
numbers.forEach(a)
输出:
4
---------------
0
9
---------------
1
16
---------------
2
25
---------------
3
  1. some() 方法测试是否至少有一个元素可以通过被提供的函数方法。该方法返回一个 Boolean 类型的值。

some() 方法用于检测数组中的元素是否满足指定条件(函数提供)。

some() 方法会依次执行数组的每个元素:

  • 如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测。
  • 如果没有满足条件的元素,则返回false。

注意: some() 不会对空数组进行检测。

注意: some() 不会改变原始数组。

var ages = [3, 10, 18, 20];

function checkAdult(age) {
    return age >= 18;
}

function myFunction() {
    document.getElementById("demo").innerHTML = ages.some(checkAdult);//true
}
  1. map() 方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。(map返回新数组!!)
const array1 = [1, 4, 9, 16];

// pass a function to map
const map1 = array1.map(x => x * 2);

console.log(map1);
// expected output: Array [2, 8, 18, 32]

17. 数组去重

一、利用ES6 Set去重(ES6中最常用)

function unique (arr) {
  return Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
 //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

二、利用for嵌套for,然后splice去重(ES5中最常用)

function unique(arr){            
        for(var i=0; i<arr.length; i++){
            for(var j=i+1; j<arr.length; j++){
                if(arr[i]==arr[j]){         //第一个等同于第二个,splice方法删除第二个
                    arr.splice(j,1);
                    j--;
                }
            }
        }
return arr;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
    //[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}]     //NaN和{}没有去重,两个null直接消失了

三、利用indexOf去重

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array = [];
    for (var i = 0; i < arr.length; i++) {
        if (array .indexOf(arr[i]) === -1) {
            array .push(arr[i])
        }
    }
    return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
   // [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]  //NaN、{}没有去重

四、利用includes

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array =[];
    for(var i = 0; i < arr.length; i++) {
            if( !array.includes( arr[i]) ) {//includes 检测数组是否有某个值
                    array.push(arr[i]);
              }
    }
    return array
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
    //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]     //{}没有去重

五、利用hasOwnProperty

利用filter + hasOwnProperty 进行去重,创建一个新对象obj,利用filter遍历数组中的每个元素,检测obj中是否有这个元素,若没有,则加入,并且另filter 返回该元素,若遍历到重复元素,则不返回。

function unique(arr) {
    var obj = {};
    return arr.filter(function(item, index, arr){
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}
    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
        console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}]   //所有的都去重了

18.null 和 undefined 的区别,如何让一个属性变为null

undefined 表示一个变量自然的、最原始的状态值,而 null 则表示一个变量被人为的设置为空对象,而不是原始状态。所以,在实际使用过程中,为了保证变量所代表的语义,不要对一个变量显式的赋值 undefined,当需要释放一个对象时,直接赋值为 null 即可。

解析:

undefined 的字面意思就是:未定义的值 。这个值的语义是,希望表示一个变量最原始的状态,而非人为操作的结果 。 这种原始状态会在以下 4 种场景中出现:

  1. 声明了一个变量,但没有赋值
  2. 访问对象上不存在的属性
  3. 函数定义了形参,但没有传递实参
  4. 使用 void 对表达式求值

因此,undefined 一般都来自于某个表达式最原始的状态值,不是人为操作的结果。当然,你也可以手动给一个变量赋值 undefined,但这样做没有意义,因为一个变量不赋值就是 undefined 。

null 的字面意思是:空值 。这个值的语义是,希望表示 一个对象被人为的重置为空对象,而非一个变量最原始的状态 。 在内存里的表示就是,栈中的变量没有指向堆中的内存对象

前端面试题--js_第5张图片

null 有属于自己的类型 Null,而不属于Object类型,typeof 之所以会判定为 Object 类型,是因为JavaScript 数据类型在底层都是以二进制的形式表示的,二进制的前三位为 0 会被 typeof 判断为对象类型,而 null 的二进制位恰好都是 0 ,因此,null 被误判断为 Object 类型。

19.数组和伪数组的区别

  1. 定义
  • 数组是一个特殊对象,与常规对象的区别:
    • 当由新元素添加到列表中时,自动更新length属性
    • 设置length属性,可以截断数组
    • 从Array.protoype中继承了方法
    • 属性为’Array’
  • 类数组是一个拥有length属性,并且他属性为非负整数的普通对象,类数组不能直接调用数组方法。
  1. 区别

    本质:类数组是简单对象,它的原型关系与数组不同。类数组转换为数组

  2. 转换方法

  • 使用 Array.from()
  • 使用 Array.prototype.slice.call()
  • 使用 Array.prototype.forEach() 进行属性遍历并组成新的数组

20. 介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

https://www.jianshu.com/p/4efa7675834c

Set

ES6 新增的一种新的的数据结构,类似于数组,但成员是唯一且无序的,没有重复的值。

  1. 成员不能重复;

  2. 只有键值,没有键名,有点类似数组;

  3. 可以遍历,方法有 add、delete、has

  4. add: 新增,相当于array里的push

  5. delete: 存在即删除集合中的value

  6. has: 判断集合中是否存在value

  7. clear: 清空集合

    let s = new Set()
    s.add(1).add(2).add(1)     // Set{1, 2}
    
    s.has(1)                   // true
    s.has(3)                   // false
    s.delete(1) 
    s.has(1)                   // false
    s.clean()                  // Set{}
    
  8. Set 结构可以利用 Array.from 或者 扩展运算符 转化为数组

    let s = new Set([1, 2, 3])
    // Array.from
    let arr = Array.from(s)
    console.log(arr)            // [1, 2, 3]
    // 扩展运算符
    console.log([...s])         // [1, 2, 3]
    

WeakSet

  1. 成员都是对象(引用);
  2. 成员都是弱引用,随时可以消失(不计入垃圾回收机制)。可以用来保存 DOM 节点,不容易造成内存泄露;
  3. 不能遍历,方法有 add、delete、has

Map

  1. 本质上是键值对的集合,类似集合;
  2. 可以遍历,方法很多,可以跟各种数据格式转换;
  3. Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者基本类型)都可以作为一个键或一个值。
const map1 = new Map();

map1.set('a', 1);
map1.set('b', 2);
map1.set('c', 3);

console.log(map1.get('a'));
// expected output: 1

map1.set('a', 97);

console.log(map1.get('a'));
// expected output: 97

console.log(map1.size);
// expected output: 3

map1.delete('b');

console.log(map1.size);
// expected output: 2

WeakMap

  1. 只接收对象为键名(null 除外),不接受其他类型的值作为键名;
  2. 键名指向的对象,不计入垃圾回收机制;
  3. 不能遍历,方法同 get、set、has、delete

21. 简单说说 js 中有哪几种内存泄露的情况

内存泄漏: 由于疏忽或错误造成未能释放已经不再使用的内存/不再用到的内存没有及时释放

  1. 意外的全局变量;解决:----使用严格模式
    前端面试题--js_第6张图片

  2. 闭包 --不用的时候让元素 = null;

  3. 未被清空的定时器—在不用的时候及时清除定时器;

  4. 未被销毁的事件监听–在不使用的时候取消事件监听;

  5. DOM 引用 —让元素 = null;

22. 同步和异步

同步

  • 指在 主线程上排队执行的任务,只有前一个任务执行完毕,才能继续执行下一个任务。
  • 也就是调用一旦开始,必须这个调用 返回结果(划重点——)才能继续往后执行。程序的执行顺序和任务排列顺序是一致的。

异步

  • 异步任务是指不进入主线程,而进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程。
  • 每一个任务有一个或多个 回调函数。前一个任务结束后,不是执行后一个任务,而是执行回调函数,后一个任务则是不等前一个任务结束就执行。
  • 程序的执行顺序和任务的排列顺序是不一致的,异步的。
  • 我们常用的setTimeout和setInterval函数,Ajax都是异步操作。
  • 异步是非阻塞的

23. 宏任务和微任务

微任务 执行时机比 宏任务 早

宏任务:setTimeout,setInterval,DOM事件,AJAX请求

微任务:Promise,async/await,process.nextTick()

微任务 > DOM渲染 > 宏任务

见到 await 必须等await执行完,才能执行之后的代码

24. promise和 async await 区别

  • 概念
    Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大,简单地说,Promise好比容器,里面存放着一些未来才会执行完毕(异步)的事件的结果,而这些结果一旦生成是无法改变的

前端面试题--js_第7张图片
前端面试题--js_第8张图片
前端面试题--js_第9张图片
前端面试题--js_第10张图片

async await也是异步编程的一种解决方案,他遵循的是Generator 函数的语法糖,他拥有内置执行器,不需要额外的调用直接会自动执行并输出结果,它返回的是一个Promise对象。
前端面试题--js_第11张图片

两者的区别

  1. Promise的出现解决了传统callback函数导致的“回调地狱”问题,但它的语法导致了它向纵向发展行成了一个回调链,遇到复杂的业务场景,这样的语法显然也是不美观的。而async await代码看起来会简洁些,使得异步代码看起来像同步代码,await的本质是可以提供等同于”同步效果“的等待异步返回能力的语法糖,只有这一句代码执行完,才会执行下一句。
  2. async await与Promise一样,是非阻塞的。
  3. async await是基于Promise实现的,可以说是改良版的Promise,它不能用于普通的回调函数。

25. 用js实现sleep,用promise

function sleep (time) {
  return new Promise((resolve) => setTimeout(resolve, time));
}
 
// 用法
sleep(500).then(() => {
    // 这里写sleep之后需要去做的事情
})

不要忘了开源的力量

const sleep = require("sleep")
const t1 = +new Date()
sleep.msleep(3000)
const t2 = +new Date()
console.log(t2 - t1)

优点:能够实现更加精细的时间精确度,而且看起来就是真的 sleep 函数,清晰直白。

26. 循环i,setTimeout 中输出什么,如何解决(块级作用域,函数作用域)

for (var i = 0; i< 10; i++){
   setTimeout(() => {
   console.log(i);
   }, 0)
} //输出10个10,而不是输出1到10

期望:输出1到10

为什么无法输出1到十

在上面的代码中,for循环是同步代码,setTimeout是异步代码。遇到这种既包含同步又包含异步的情况,JavaScript依旧按照从上到下的顺序执行同步代码,并将异步代码插入任务队列。setTimeout的第二个参数则是把执行代码(console.log(i))添加到任务队列需等待的毫秒数,但等待的时间是相对主程序完毕的时间计算的,也就是说,在执行到setTimeout函数时会等待一段时间,再将当前任务插入任务队列。
 最后,当执行完同步代码,js引擎就会去执行任务队列中的异步代码。这时候任务队列中就会有十个console.log(i)。我们知道,在每次循环中将setTimeout里面的代码“console.log(i)”放入任务队列时,i的值都是不一样的。但JavaScript引擎开始执行任务队列中的代码时,会开始在当前的作用域中开始找变量i,但是当前作用域中并没有对变量i进行定义。这个时候就会在创造该函数的作用域中寻找i。创建该函数的作用域就是全局作用域,这个时候就找到了for循环中的变量i,这时的i是全局变量,并且值已经确定:10。十个console.log“共享”i的值。这就是作用域链的问题。

解决方法

for (let i = 0; i< 10; i++){
   setTimeout(() => {
      console.log(i)
   }, 1000);
}

最优解决方案,利用let形成块级作用域

27. this 的指向有哪些

1、普通函数中的this

谁调用了函数或者方法,那么这个函数或者对象中的this就指向谁

let getThis = function () {
    console.log(this);
}
 
let obj={
    name:"Jack",
    getThis:function(){
        console.log(this);
    }
}
//getThis()方法是由window在全局作用域中调用的,所以this指向调用该方法的对象,即window
getThis();//window
//此处的getThis()方法是obj这个对象调用的,所以this指向obj
obj.getThis();//obj
  1. 匿名函数中的this:匿名函数的执行具有全局性,则匿名函数中的this指向是window,而不是调用该匿名函数的对象;
let obj = {
    getThis: function () {
        return function () {
            console.log(this);
        }
    }
}
obj.getThis()(); //window

上面代码中,getThis()方法是由obj调用,但是obj.getThis()返回的是一个匿名函数,而匿名函数中的this指向window,所以打印出window。 如果想在上述代码中使this指向调用该方法的对象,可以提前把this传值给另外一个变量(_this或者that):

let obj = {
     getThis: function () {
     //提前保存this指向
         let _this=this
         return function () {
             console.log(_this);
         }
     }
 }
 obj.getThis()(); //obj

3.箭头函数中的this

  1. 箭头函数中的this是在函数定义的时候就确定下来的,而不是在函数调用的时候确定的;
  2. 箭头函数中的this指向父级作用域的执行上下文;(技巧:因为javascript中除了全局作用域,其他作用域都是由函数创建出来的,所以如果想确定this的指向,则找到离箭头函数最近的function,与该function平级的执行上下文中的this即是箭头函数中的this
  3. 箭头函数无法使用apply、call和bind方法改变this指向,因为其this值在函数定义的时候就被确定下来。

例1:首先,距离箭头函数最近的是getThis(){},与该函数平级的执行上下文是obj中的执行上下文,箭头函数中的this就是下注释代码处的this,即obj。

let obj = {
    //此处的this即是箭头函数中的this
    getThis: function () {
        return  ()=> {
            console.log(this);
        }
    }
}
obj.getThis()(); //{ getThis: [Function: getThis] }

例2:该段代码中存在两个箭头函数,this找不到对应的function(){},所以一直往上找直到指向window。

//代码中有两个箭头函数,由于找不到对应的function,所以this会指向window对象。
let obj = {
     getThis: ()=> {
         return  ()=> {
             console.log(this);
         }
     }
 }
 obj.getThis()(); //window

28. 常用的字符串方法有哪些?

1. charAt() 方法从一个字符串中返回指定索引的字符。

var str = "HELLO WORLD";
var n = str.charAt(2)//L

2. concat() 方法将一个或多个字符串与原字符串连接合并,形成一个新的字符串并返回。

var str1 = "Hello ";
var str2 = "world!";
var n = str1.concat(str2);//Hello world!

3. includes() 方法用于判断一个字符串是否包含在另一个字符串中,根据情况返回 true 或 false。

var str = "Hello world, welcome to the Runoob。";
var n = str.includes("Runoob");//true

4. indexOf() 方法返回调用它的 String 对象中第一次出现的指定值的索引,从 fromIndex 处进行搜索。如果未找到该值,则返回 -1。 match() 方法检索返回一个字符串匹配正则表达式的的结果。

var str="Hello world, welcome to the universe.";
var n=str.indexOf("welcome");//13

5. padStart() 方法用另一个字符串填充当前字符串(重复,如果需要的话),以便产生的字符串达到给定的 长度。填充从当前字符串的开始(左侧)应用的。 (常用于时间补 0)

'abc'.padStart(10);         // "       abc"
'abc'.padStart(10, "foo");  // "foofoofabc"
'abc'.padStart(6,"123465"); // "123abc"
'abc'.padStart(8, "0");     // "00000abc"
'abc'.padStart(1);          // "abc"

6. replace() 方法返回一个由替换值( replacement )替换一些或所有匹配的模式( pattern )后的新 字符串。模式可以是一个字符串或者一个正则表达式,替换值可以是一个字符串或者一个每次匹配都要 调用的回调函数。 原字符串不会改变。 slice() 方法提取某个字符串的一部分,并返回一个新的字符串,且不会改动原字符串。

在本例中,我们将执行一次替换,当第一个 "Microsoft" 被找到,它就被替换为 "Runoob"var str="Visit Microsoft! Visit Microsoft!";
var n=str.replace("Microsoft","Runoob");//Visit Runoob!Visit Microsoft!

7. split() 方法使用指定的分隔符字符串将一个 String 对象分割成字符串数组,以将字符串分隔为 子字符串,以确定每个拆分的位置。 substr() 方法返回一个字符串中从指定位置开始到指定字符数的字符。trim() 方法会从一个字符串的两端删除空白字符。在这个上下文中的空白字符是所有的空白字符 (space, tab, no-break space 等) 以及所有行终止符字符(如 LF,CR)。

var str="How are you doing today?";
var n=str.split(" ");//How,are,you,doing,today?
var str="Hello world!";
var n=str.substr(2,3)//llo
var str = "       Runoob        ";
alert(str.trim());//Runoob

29. 什么是闭包?手写一个闭包函数? 闭包有哪些优缺点?

闭包(closure)指有权访问另一个函数作用域中变量的函数。简单理解就是 ,一个作用域可以访问另外一个函数内部的局部变量

因为外层函数拿不到内层函数的值,所以可以在一个函数里面再定义一个函数,用这个函数获取到外层函数中的局部变量,那么只要将内部的函数作为返回值,就可以在函数的外部读取到函数内部的变量。

function fn() {
  var num = 10
  function fun() {
    console.log(num)
  }
  return fun
}
var f = fn()
f()

作用: 延长变量作用域、在函数的外部可以访问函数内部的局部变量,容易造成内存泄露,因为闭包中的局部变量永远不会被回收

闭包的特点:

让外部访问函数内部变量成为可能;
​ 可以避免使用全局变量,防止全局变量污染;
​ 可以让局部变量常驻在内存中;
​ 会造成内存泄漏(有一块内存空间被长期占用,而不被释放)

30. js 继承

  • 继承的好处
    • a:提高了代码的复用性
    • b:提高了代码的可维护性
    • c:让类与类之间产生了关系,是多态的前提
  • 继承的弊端
    • 类的耦合性增强了,但是开发的原则:高内聚,低耦合。

原型链继承

  • 通过实例化一个新的函数,子类的原型指向了父类的实例,子类就可以调用其父类原型对象上的私有属性和公有方法。(本质就是重写了原型对象)

原型链继承的优点

  • 简单,易实现
  • 父类新增原型方法/原型属性,子类都能访问

原型链继承的缺点

  • 无法实现多继承
  • 引用类型的值会被实例共享
  • 子类型无法给超类型传递参数

鉴于这些缺点,实践中很少会单独使用原型链继承。

借用构造函数继承(对象冒充)

  • 借用构造函数的基本思想就是利用 call 或者 apply 把父类中通过 this 指定的属性和方法复制(借用)到子类创建的实例中。

借用构造函数的优点

  • 解决了引用类型的值被实例共享的问题
  • 可以向超类传递参数
  • 可以实现多继承(call 若干个超类)

借用构造函数的缺点

  • 不能继承超类原型上的属性和方法
  • 无法实现函数复用,由于 call 有多个父类实例的副本,性能损耗。
  • 原型链丢失

寄生组合式继承
顾名思义,寄生式 + 组合式。它是寄生式继承的加强版。这也是为了避免组合继承中无可避免地要调用两次父类构造函数的最佳方案。

开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

本质是子类的原型继承自父类的原型,申明一个用于继承原型的 inheritPrototype 方法,通过这个方法我们能够将子类的原型指向超类的原型,从而避免超类二次实例化。

class 继承
ES6 中,通过 class 关键字来定义类,子类可以通过 extends 继承父类。

详情见
https://cloud.tencent.com/developer/article/1851143

31. 用什么实现跨域

前端开发中如何解决跨域问题_哔哩哔哩_bilibili

**同源:**协议,主机和端口都完全一样,就是同源

1、 jsonp: 利用标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP 请求一 定需要对方的服务器做支持才可以。

JSONP 优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持 get 方法具有局限性,不安全可能会遭受 XSS 攻击。

声明一个回调函数,其函数名(如 show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。 创建一个 `` 标签,把那个跨域的 API 数据接口地址,赋值给 script 的 src,还要在这个地址中向服 务器传递该函数名(可以通过问号传参:?callback=show)。

服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是 show,它准备好的数据是 show(‘我不爱你’) 。

最后服务器把准备的数据通过 HTTP 协议返回给客户端,客户端再调用执行之前声明的回调函数 (show),对返回的数据进行操作。

2、在后端解决跨域问题:cors

前端面试题--js_第12张图片

若后端使用Nodejs,可以使用cors中间件来解决跨域问题,cors默认允许所有跨域请求,可以通过配置origin属性,在里面添加允许跨域的域名。

CORS:跨域资源共享(CORS)是一种机制;当一个资源访问到另外一个资源(这个资源放在 不同的域名或者不同的协议或者端口),资源就会发起一个跨域的 HTTP 请求需要浏览器和服务器同时支持;

1.整个 CORS 通信,都是浏览器自动完成。浏览器发现了 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉;

2.实现 CORS 的关键是服务器,只要服务器实现了 CORS 接口,就可以跨源通信

3.服务器对于不同的请求,处理方式不一样; 有简单请求和非简单请求

3、在前端解决跨域问题:配置代理

前端面试题--js_第13张图片

因为跨域是浏览器的保护机制,那么只要脱离浏览器发送请求,就不会收到跨域保护机制的影响,所以可以使用一个中转服务器来发送请求和接收响应,前端只要接受中转服务器的响应,和中转服务器的url保持一致就可以了。

32. cookie,localstorage, sessionstorage

与服务器交互:

  • cookie 是网站为了标示用户身份而储存在用户本地终端上的数据(通常经过加密)
  • cookie 始终会在同源 http 请求头中携带(即使不需要),在浏览器和服务器间来回传递
  • sessionStorage 和 localStorage 不会自动把数据发给服务器,仅在本地保存

存储大小:

cookie 数据根据不同浏览器限制,大小一般不能超过 4k

sessionStorage 和 localStorage 虽然也有存储大小的限制,但比 cookie 大得多,可以达到 5M 或更大

有期时间:

  • localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据
  • sessionStorage 数据在当前浏览器窗口关闭后自动删除
  • cookie 设置的 cookie 过期时间之前一直有效,与浏览器是否关闭无关
  1. 浏览器的大小不统一,并且在IE8以上的IE版本才支持localStorage这个属性
  2. 目前所有的浏览器中都会把localStorage的值类型限定为string类型,这个在对我们日常比较常见的JSON对象类型需要一些转换
  3. localStorage在浏览器的隐私模式下面是不可读取的
  4. localStorage本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡
  5. localStorage不能被爬虫抓取到

33. Cookie 和 Session的关系和区别

1、cookie 数据存放在客户的浏览器上,session 数据放在服务器上。

2、cookie 不是很安全,别人可以分析存放在本地的 COOKIE 并进行 COOKIE 欺骗

考虑到安全应当使用 session。

3、session 会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能

考虑到减轻服务器性能方面,应当使用 COOKIE。

4、单个 cookie 保存的数据不能超过 4K,很多浏览器都限制一个站点最多保存 20 个 cookie。

2. cookie 能做什么?

  • 用户在第一次登录某个网站时,要输入用户名密码,如果觉得很麻烦,下次登录时不想输入了,那么就在第一次登录时将登录信息存放在 cookie 中。下次登录时我们就可以直接获取 cookie 中的用户名密码来进行登录。

    PS:虽然 浏览器将信息保存在 cookie 中是加密了,但是可能还是会造成不安全的信息泄露

  • 类似于购物车性质的功能,第一次用户将某些商品放入购物车了,但是临时有事,将电脑关闭了,下次再次进入此网站,我们可以通过读取 cookie 中的信息,恢复购物车中的物品。

    PS:实际操作中,这种方法很少用了,基本上都是将这些信息存储在数据库中。然后通过查询数据库的信息来恢复购物车里的物品

  • 页面之间的传值。在实际开发中,我们往往会通过一个页面跳转到另外一个页面。后端服务器我们可以通过数据库,session 等来传递页面所需要的值。但是在浏览器端,我们可以将数据保存在 cookie 中,然后在另外页面再去获取 cookie 中的数据。

    PS:这里要注意 cookie 的时效性,不然会造成获取 cookie 中数据的混乱。

34. js 的执行机制是怎么样的?

js 是一个单线程、异步、非阻塞 I/O 模型、 event loop 事件循环的执行机制

所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。异步 任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程, 某个异步任务可以执行了,该任务才会进入主线程执行。

35. 什么是深拷贝、什么是浅拷贝?以及实现方式

浅拷贝: 创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个 地址,就会影响到另一个对象。

深拷贝: 会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即 发生深拷贝。 深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

实现浅拷贝方法

(1)Object.assign方法

var obj = {
    a: 1,
    b: 2
}
var obj1 = Object.assign({},obj);
boj1.a = 3;
console.log(obj.a) // 3

(2)for in方法

// 只复制第一层的浅拷贝
function simpleCopy(obj1) {
   var obj2 = Array.isArray(obj1) ? [] : {};
   for (let i in obj1) {
   obj2[i] = obj1[i];
  }
   return obj2;
}
var obj1 = {
   a: 1,
   b: 2,
   c: {
         d: 3
      }
}
var obj2 = simpleCopy(obj1);
obj2.a = 3;
obj2.c.d = 4;
alert(obj1.a); // 1
alert(obj2.a); // 3
alert(obj1.c.d); // 4
alert(obj2.c.d); // 4

实现深拷贝方法

(1)采用递归去拷贝所有层级属性

function deepClone(obj){
    let objClone = Array.isArray(obj)?[]:{};
    if(obj && typeof obj==="object"){
        for(key in obj){
            if(obj.hasOwnProperty(key)){
                //判断ojb子元素是否为对象,如果是,递归复制
                if(obj[key]&&typeof obj[key] ==="object"){
                    objClone[key] = deepClone(obj[key]);
                }else{
                    //如果不是,简单复制
                    objClone[key] = obj[key];
                }
            }
        }
    }
    return objClone;
}    
let a=[1,2,3,4],
    b=deepClone(a);
a[0]=2;
console.log(a,b);

(2)使用JSON.stringify和JSON.parse实现深拷贝:JSON.stringify把对象转成字符串,再用JSON.parse把字符串转成新的对象;

function deepCopy(obj1){
    let _obj = JSON.stringify(obj1);
    let obj2 = JSON.parse(_obj);
    return obj2;
  }
    var a = [1, [1, 2], 3, 4];
    var b = deepCopy(a);
    b[1][0] = 2;
    alert(a); // 1,1,2,3,4
    alert(b); // 2,2,2,3,4

(3)热门的函数库lodash,也有提供_.cloneDeep用来做深拷贝;

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);
// false

36. 请写出至少两种常见的数组排序的方法(原生 js)

// 快速排序:取一个值作为参考值,比参考值小的放左边,比参考值大的放右边,同时不断递归,实现排序
function quickSort(elements) {
    // 递归出口
    if (elements.length <= 1) {
        return elements;
    }
    // 取数组中间值为初始的索引值
    var pivotIndex = Math.floor(elements.length / 2);
    // 取出中间索引对应的值作为参考值
    var pivot = elements.splice(pivotIndex, 1)[0];
    var left = [];
    var right = [];
    // 比参考值小的放到左边,比参考值大的放到右边
    for (var i = 0; i < elements.length; i++) {
        if (elements[i] < pivot) {
            left.push(elements[i]);
        } else {
            right.push(elements[i]);
        }
    }
    return quickSort(left).concat([pivot],quickSort(right));
}
var elements = [3,5,6,8,2,4,7,9,1,10];
console.log(quickSort(elements))

// 插入排序
function sort(elements) {
    // 假设第0个元素是一个有序数列,第1个以后的是无序数列
    // 所以从第1个元素开始将无序数列的元素插入到有序数列中
    for (var i = 1; i <= elements.length; i++) {
        // 升序
        // 遇到第一个无序的数字了
        if (elements[i] < elements[i - 1]) {
            // 把这个无序的数字存储起来
            var guard = elements[i];
            // 记住有序数列的最后一个位置,将有序数列的最后一个数往后挪一位,给前面找出来的那个无序的小数字腾个空
            var j = i - 1;
            elements[i] = elements[j];
            // 至于 guard具体要插入到哪里,就需要一个个与前面的有序数字比较了,如果guard比这些数字小,这些数字就都后移一位
            while (i >= 0 && guard < elements[j]) {
                elements[j + 1] = elements[j];
                j--;
            }
            // 找到插入的位置了
            elements[j + 1] = guard;
        }
    }
}

// 冒泡排序
function sort(elements) {
    for (var i = 0; i < ekements.length - 1;i++) {
        for (var j = 0; j < elements.length - 1 -i; j++) {
            if (elements[j] > elements[j + 1]) {
                var swap = elements[j];
                elements[j] = elements[j + 1];
                elements[j + 1] = swap;
            }
        }
    }
}

37. es6中箭头函数

// 箭头函数
let fun = (name) => {
    // 函数体
    return `Hello ${name} !`;
};
 
// 等同于
let fun = function (name) {
    // 函数体
    return `Hello ${name} !`;
};

可以看出,定义箭头函在数语法上要比普通函数简洁得多。箭头函数省去了function关键字,采用箭头=>来定义函数。函数的参数放在=>前面的括号中,函数体跟在=>后的花括号中。

关于箭头函数的参数:

如果箭头函数没有参数,直接写一个空括号即可。

如果箭头函数的参数只有一个,也可以省去包裹参数的括号。

如果箭头函数有多个参数,将参数依次用逗号(,)分隔,包裹在括号中即可。

// 没有参数
let fun1 = () => {
    console.log(111);
};
 
// 只有一个参数,可以省去参数括号
let fun2 = name => {
    console.log(`Hello ${name} !`)
};
 
// 有多个参数
let fun3 = (val1, val2, val3) => {
    return [val1, val2, val3];
};

关于箭头函数的函数体:

如果箭头函数的函数体只有一句代码,就是简单返回某个变量或者返回一个简单的JS表达式,可以省去函数体的大括号{ }。

let f = val => val;
// 等同于
let f = function (val) { return val };
 
let sum = (num1, num2) => num1 + num2;
// 等同于
let sum = function(num1, num2) {
  return num1 + num2;
};

如果箭头函数的函数体只有一句代码,就是返回一个对象,可以像下面这样写:

// 用小括号包裹要返回的对象,不报错
let getTempItem = id => ({ id: id, name: "Temp" });
 
// 但绝不能这样写,会报错。
// 因为对象的大括号会被解释为函数体的大括号
let getTempItem = id => { id: id, name: "Temp" };

如果箭头函数的函数体只有一条语句并且不需要返回值(最常见是调用一个函数),可以给这条语句前面加一个void关键字

let fn = () => void doesNotReturn();

38. 箭头函数有哪些特征,请简单描述一下它?

1. 箭头函数没有自己的 this,this 指向定义箭头函数时所处的外部执行环境的 this,即使调用call/apply/bind也无法改变箭头函数的 this

!!! 箭头函数继承而来的this指向永远不变(重要!!深入理解!!)

  1. 箭头函数是匿名函数,

  2. 箭头函数不能作为构造函数使用,箭头函数不能 new,会报错

  3. 箭头函数没有 arguments,在箭头函数内访问这个变量访问的是外部执行环境的 arguments ,可以在箭头函数中使用rest参数代替arguments对象,来访问箭头函数的参数列表!!
    5. 箭头函数没有 prototype

  4. 箭头函数不能用作Generator函数,不能使用yeild关键字

39. call、apply、bind 三者的异同

共同点 : 都可以改变 this 指向;

不同点: call 和 apply 会调用函数, 并且改变函数内部 this 指向. call 和 apply传递的参数不一样,call 传递参数使用逗号隔开,apply 使用数组传递 ,bind 不会调用函数, 可以改变函 数内部 this 指向.

应用场景

  1. call 经常做继承.
  2. apply 经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值
  3. bind 不调用函数,但是还想改变 this 指向. 比如改变定时器内部的 this 指向

作用:

都可以改变函数内部的this指向。

区别点:

  1. call 和 apply 会调用函数,并且改变函数内部this指向。
  2. call 和 apply 传递的参数不一样,call 传递参数arg1,arg2…形式 apply 必须数组形式[arg]
  3. bind 不会调用函数,可以改变函数内部this指向。

解析:

  1. call方法

    JavaScript 函数 Call (w3school.com.cn)

改变函数内部this指向

call()方法调用一个对象。简单理解为调用函数的方式,但是它可以改变函数的this指向。

写法:fun.call(thisArg, arg1, arg3, …) // thisArg为想要指向的对象,arg1,arg3为参数

call 的主要作用也可以实现继承

function Person(uname, age) {
    this.uname = uname;
    this.age = age;
  }
  function Son(uname, age) {
    Person.call(this, uname, age);
  }
  var son = new Son("zhang", 12);
  console.log(son);//Son { uname: 'zhang', age: 12 }
  1. apply方法

    JavaScript 函数 Apply (w3school.com.cn)

apply()方法调用一个函数。简单理解为调用函数的方式,但是它可以改变函数的this指向。

写法:fun.apply(thisArg, [argsArray])

  • thisArg:在fun函数运行时指定的this值
  • argsArray:传递的值,必须包含在数组里面
  • 返回值就是函数的返回值,因为他就是调用函数

apply的主要应用,比如可以利用apply可以求得数组中最大值

const arr = [1, 22, 3, 44, 5, 66, 7, 88, 9];
const max = Math.max.apply(Math, arr);
console.log(max);
  1. bind方法

bind()方法不会调用函数,但是能改变函数内部this指向

写法:fun.bind(thisArg, arg1, arg2, …)

  • thisArg:在fun函数运行时指定的this值
  • arg1,arg2:传递的其他参数
  • 返回由指定的this值和初始化参数改造的原函数拷贝
var o = { name:  "lisa" };
 function fn() {
 	console.log( this );
 }
 var f = fn.bind(o);
 f();

bind应用

如果有的函数我们不需要立即调用,但是又需要改变这个函数的this指向,此时用bind再合适不过了

const btns = document.querySelectorAll("button");
for(let i = 0; i < btns.length; i++){
 	btns[i].onclick = function() {
 	this.disabled = true;
 	setTimeout(function() {
 		this.disabled = false;
 		}.bind(this),2000);
 	};
 }

扩展:

主要应用场景:

  1. call 经常做继承。
  2. apply 经常跟数组有关系,比如借助于数学对象实现数组最大值最小值。
  3. bind 不调用函数,但是还想改变this指向,比如改变定时器内部的this指向。

40. constructor的理解

在 JavaScript 中, constructor 属性返回对象的构造函数

返回值是函数的引用,不是函数名:

JavaScript 数组 constructor 属性返回 function Array() { [native code] }

JavaScript 数字 constructor 属性返回 function Number() { [native code] }

JavaScript 字符串 constructor 属性返回 function String() { [native code] }

如果一个变量是数组你可以使用 constructor 属性来定义。

41. new会发生什么

  1. 创建空对象;
    var obj = {};
  2. 设置新对象的constructor属性为构造函数的名称,设置新对象的proto属性指向构造函数的prototype对象;
    obj.proto = ClassA.prototype;
    扩展了新对象的原型链。
  3. 使用新对象调用构造函数,构造函数中的this被指向新实例对象:
    ClassA.call(obj);  //{}.构造函数();
  4. 返回this指针。当存在显示的返回时,返回return后面的内容。新建的空对象作废。
function test() {
 this.name = "test";
 }
 test.prototype = {
 a:{},
 b:{}
 }

var  c = new test();

42. 为什么js是单线程

这主要和js的用途有关,js是作为浏览器的脚本语言,主要是实现用户与浏览器的交互,以及操作dom;这决定了它只能是单线程,否则会带来很复杂的同步问题。 举个例子:如果js被设计了多线程,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,此时浏览器就会一脸茫然,不知所措。所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

43. 死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源而造成阻塞的现象,若无外力作用,它们都将无法继续执行

产生原因

  • 竞争资源引起进程死锁
  • 可剥夺和非剥夺资源
  • 竞争非剥夺资源
  • 竞争临时性资源
  • 进程推进顺序不当

产生条件

  1. 互斥条件:涉及的资源是非共享的
    • 涉及的资源是非共享的,一段时间内某资源只由一个进程占用,如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放
  2. 不剥夺条件:不能强行剥夺进程拥有的资源
    • 进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放
  3. 请求和保持条件:进程在等待一新资源时继续占有已分配的资源
    • 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放 环路等待条件:存在一种进程的循环链,链中的每一个进程已获得的资源同时被链中的下一个进程所请求 在发生死锁时,必然存在一个进程——资源的环形链

解决办法

只要打破四个必要条件之一就能有效预防死锁的发生

44. 面向对象的三个特征,分别说一下什么意思

参考答案:

概念:

封装: 将对象运行所需的资源封装在程序对象中——基本上,是方法和数据。对象是“公布其接口”。其他附加到这些接口上的对象不需要关心对象实现的方法即可使用这个对象。这个概念就是“不要告诉我你是怎么做的,只要做就可以了。”对象可以看作是一个自我包含的原子。对象接口包括了公共的方法和初始化数据。

继承: 继承可以解决代码复用,让编程更加靠近人类思维。当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过继承父类中的属性和方法。

多态: 多态是指一个引用(类型)在不同情况下的多种状态。也可以理解成:多态是指通过指向父类的引用,来调用在不同子类中实现的方法。

特点:

封装可以隐藏实现细节,使得代码模块化;

继承可以扩展已存在的代码模块(类),它们的目的都是为了——代码重用。

多态就是相同的事物,调用其相同的方法,参数也相同时,但表现的行为却不同。多态分为两种,一种是行为多态与对象的多态

45. 防抖和节流的原理和使用场景

函数防抖和函数节流:优化高频率执行js代码的一种手段,js中的一些事件如浏览器的resize、scroll,鼠标的mousemove、mouseover,input输入框的keypress等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。为了优化体验,需要对这类事件进行调用次数的限制。

防抖:一个事件 发生 一定时间 之后,才执行 特定动作

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

节流: https://segmentfault.com/a/1190000019577510

一定时间 之内,限制 一个动作执行一次

主要实现思路就是通过 setTimeout定时器,通过设置延时时间,在第一次调用时,创建定时器,先设定一个变量true,写入需要执行的函数。第二次执行这个函数时,会判断变量是否true,是则返回。当第一次的定时器执行完函数最后会设定变量为false。那么下次判断变量时则为false,函数会依次运行。目的在于在一定的时间内,保证多次函数的请求只执行最后一次调用。

https://www.nowcoder.com/study/live/691/2/16

46. 获取当前页面url

  1. window.location.href (设置或获取整个 URL 为字符串)
var test = window.location.href;
alert(test);
//  返回:http://i.cnblogs.com/EditPosts.aspx?opt=1
  1. window.location.protocol (设置或获取 URL 的协议部分)
var test = window.location.protocol;
alert(test);
//返回:http:
  1. window.location.host (设置或获取 URL 的主机部分)
var test = window.location.host;
alert(test);
//返回:i.cnblogs.com
  1. window.location.port (设置或获取与 URL 关联的端口号码)
var test = window.location.port;
alert(test);
//返回:空字符(如果采用默认的80端口 (update:即使添加了:80),那么返回值并不是默认的80而是空字符)
  1. window.location.pathname (设置或获取与 URL 的路径部分(就是文件地址))
var test = window.location.pathname;
alert(test);
//返回:/EditPosts.aspx
  1. window.location.search (设置或获取 href 属性中跟在问号后面的部分)
var test = window.location.search;
alert(test);
//返回:?opt=1PS:获得查询(参数)部分,除了给动态语言赋值以外,我们同样可以给静态页面,并使用javascript来获得相信应的参数值。)
  1. window.location.hash (设置或获取 href 属性中在井号“#”后面的分段)
var test = window.location.hash;
alert(test);
//返回:空字符(因为url中没有)
  1. js获取url中的参数值

    正则法

 function getQueryString(name) {
          var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i');
          var r = window.location.search.substr(1).match(reg);

          if (r != null) {
             return unescape(r[2]);
          }
          return null;
  }
// 这样调用:
alert(GetQueryString("参数名1"));
alert(GetQueryString("参数名2"));
alert(GetQueryString("参数名3"));

split拆分法

function GetRequest() {
         var url = location.search; //获取url中"?"符后的字串
         var theRequest = new Object();

         if (url.indexOf("?") != -1) {
                 var str = url.substr(1);
                 strs = str.split("&");
              for(var i = 0; i < strs.length; i ++) {
                      theRequest[strs[i].split("=")[0]] = unescape(strs[i].split("=")[1]);
               }
         }
     return theRequest;
 }
var Request = new Object();
Request = GetRequest();<br>// var id=Request["id"]; 
// var 参数1,参数2,参数3,参数N;
// 参数1 = Request['参数1'];
// 参数2 = Request['参数2'];
// 参数3 = Request['参数3'];
// 参数N = Request['参数N'];

指定取
比如说一个url:http://i.cnblogs.com/?j=js, 我们想得到参数j的值,可以通过以下函数调用。

function GetQueryString(name) { 
         var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i"); 
         var r = window.location.search.substr(1).match(reg); //获取url中"?"符后的字符串并正则匹配
         var context = ""; 

     if (r != null) 
     context = r[2]; 
    reg = null; 
    r = null; 
    return context == null || context == "" || context == "undefined" ? "" : context; 
 }
alert(GetQueryString("j"));

单个参数的获取方法

function GetRequest() {
         var url = location.search; //获取url中"?"符后的字串
         if (url.indexOf("?") != -1) {? //判断是否有参数
                  var str = url.substr(1); //从第一个字符开始 因为第0个是?号 获取所有除问号的所有符串
                  strs = str.split("=");? //用等号进行分隔 (因为知道只有一个参数 
                                          //所以直接用等号进分隔 如果有多个参数 要用&号分隔 再用等号进行分隔)
                  alert(strs[1]);???? //直接弹出第一个参数 (如果有多个参数 还要进行循环的)
         }
  }

47. js中两个数组怎么取交集+(差集、并集、补集)

  1. 最普遍的做法

使用 ES5 语法来实现虽然会麻烦些,但兼容性最好,不用考虑浏览器 JavaScript 版本。也不用引入其他第三方库。

直接使用 filter、concat 来计算

var a = [1,2,3,4,5]
var b = [2,4,6,8,10]
//交集
var c = a.filter(function(v){ return b.indexOf(v) > -1 })
//差集
var d = a.filter(function(v){ return b.indexOf(v) == -1 })
//补集
var e = a.filter(function(v){ return !(b.indexOf(v) > -1) })
        .concat(b.filter(function(v){ return !(a.indexOf(v) > -1)}))
//并集
var f = a.concat(b.filter(function(v){ return !(a.indexOf(v) > -1)}));
  1. 使用 ES6 语法实现

ES6 中可以借助扩展运算符()以及 Set 的特性实现相关计算,代码也会更加简单些。

var a = [1,2,3,4,5]
var b = [2,4,6,8,10]
console.log("数组a:", a);
console.log("数组b:", b);

var sa = new Set(a);
var sb = new Set(b);

// 交集
let intersect = a.filter(x => sb.has(x));

// 差集
let minus = a.filter(x => !sb.has(x));

// 补集
let complement  = [...a.filter(x => !sb.has(x)), ...b.filter(x => !sa.has(x))];

// 并集
let unionSet = Array.from(new Set([...a, ...b]));

48. 写一个判断是否是空对象的函数

function isEmpty(value) {
    return (
        value === null || value === undefined ||
        (typeof value === 'object' && Object.keys(value).length === 0) 
    )
}

49. 字符串中的单词逆序输出(手写)

方法一:

function strReverse(str) {
   return str.split("").reverse().join("")
}

方法二:

function strReverse(str) {
    var i=str.length;
    var nstr = "";
    i=i-1;
    for (var x = i; x >=0; x--) {
        nstr+=str.charAt(x)
    }
    return nstr
}

方法三:

function strReverse(str) {
      if(str.length == 0)return null;
      var i = str.length;
      var dstr = "";
      while(--i >= 0)
      {
          dstr += str.charAt(i); 
      }
      return dstr;
}

方法四:

function strReverse(str) {
    //先将字符串分割成一个个字符,然后使用reduce将字符反向拼接
  return str.split('').reduce((prev, next) => next + prev);
}

方法五:

function strReverse(str) {
    var newstr="";
           for(var i=0;i<str.length;i++){
               newstr=str.charAt(i) + newstr;
           }
           return newstr
}

方法六:

function strReverse(str) {
            if(str.length===1){
                return str
            }
    //每次将最后的单词拼到头部,并且递归字符串(每次删除最后一个字符)
            return str.slice(-1)+strReverse(str.slice(0,-1));
}

50. 给定一个字符串,请你找出其中不含有重复字符的最长子串的长度

对字符串进行遍历,使用String.prototype.indexOf()实时获取遍历过程中的无重复子串并存放于str,并保存当前状态最长无重复子串的长度为res,当遍历结束时,res的值即为无重复字符的最长子串的长度。

/**
 * @param {string} s
 * @return {number}
 */
var lengthOfLongestSubstring = function(s) {
    var res = 0; // 用于存放当前最长无重复子串的长度
    var str = ""; // 用于存放无重复子串
    var len = s.length;
    for(var i = 0; i < len; i++) {
      var char = s.charAt(i);
      var index = str.indexOf(char);
      if(index === -1) {
         //若是无重复的字符就加入字符串
        str += char;
        res = res < str.length ? str.length : res;
      } else {
         //遇到了重复的字符,只保留第一个重复字符的后一个字符开始的字符串内容加当前字符串
        str = str.substr(index + 1) + char;
      }
    }
    return res; 
};

51. 判断输出console.log([] == []) console.log([] == 0)

console.log([]==[]);  // false
console.log([]== 0);  // true

原始值的比较是值的比较:
它们的值相等时它们就相等(==)
对象和原始值不同,对象的比较并非值的比较,而是引用的比较:
即使两个对象包含同样的属性及相同的值,它们也是不相等的
即使两个数组各个索引元素完全相等,它们也是不相等的,所以[]!=[]

[]==0,是数组进行了隐式转换,空数组会转换成数字0,所以相等

52. 三数之和

思路:排序,去重,双指针。

给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。

注意:答案中不可以包含重复的三元组。

//例如, 给定数组 nums = [-1, 0, 1, 2, -1, -4],
//满足要求的三元组集合为:
[
  [-1, 0, 1],
  [-1, -1, 2]
]

解答

这题我们才用排序+双指针的思路来做,遍历排序后的数组,定义指针l和r,分别从当前遍历元素的下一个元素和数组的最后一个元素往中间靠拢,计算结果跟目标对比。

var threeSum = function(nums) {
    if(nums.length < 3){
        return [];
    }

    let res = [];
    // 排序
    nums.sort((a, b) => a - b);
    for(let i = 0; i < nums.length; i++){
        if(i > 0 && nums[i] == nums[i-1]){
            // 去重
            continue;
        }
        if(nums[i] > 0){
            // 若当前元素大于0,则三元素相加之后必定大于0
            break;
        }
        // l为左下标,r为右下标
        let l = i + 1; r = nums.length - 1;
        while(l<r){
            let sum = nums[i] + nums[l] + nums[r];
            if(sum == 0){
                res.push([nums[i], nums[l], nums[r]]);
                while(l < r && nums[l] == nums[l+1]){
                    l++
                }
                while(l < r && nums[r] == nums[r-1]){
                    r--;
                }
                l++;
                r--;
            }
            else if(sum < 0){
                l++;
            }
            else if(sum > 0){
                r--;
            }
        }
    }

    return res;
};

53. 浏览器如何渲染页面的?

  1. HTML 被 HTML 解析器解析成 DOM 树;

  2. CSS 被 CSS 解析器解析成 CSSOM 树;

  3. 结合 DOM 树和 CSSOM 树,生成一棵渲染树(Render Tree),这一过程称为 Attachment;

  4. 生成布局(flow),浏览器在屏幕上“画”出渲染树中的所有节点;

  5. 将布局绘制(paint)在屏幕上,显示出整个页面。

不同的浏览器内核不同,所以渲染过程不太一样。

54. 重绘、重排区别如何避免

  1. 重排(Reflow):当渲染树的一部分必须更新并且节点的尺寸发生了变化,浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。

  2. 重绘(Repaint):是在一个元素的外观被改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。比如改变某个元素的背景色、文字颜色、边框颜色等等

  3. 区别:重绘不一定需要重排(比如颜色的改变),重排必然导致重绘(比如改变网页位置)

  4. 引发重排

    4.1 添加、删除可见的dom

    4.2 元素的位置改变

    4.3 元素的尺寸改变(外边距、内边距、边框厚度、宽高等几何属性)

    4.4 页面渲染初始化

    4.5 浏览器窗口尺寸改变

    4.6 获取某些属性。当获取一些属性时,浏览器为取得正确的值也会触发重排,它会导致队列刷新,这些属性包括:offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight、getComputedStyle() (currentStyle in IE)。所以,在多次使用这些值时应进行缓存。

  5. 优化:

    浏览器自己的优化:

    浏览器会维护1个队列,把所有会引起重排,重绘的操作放入这个队列,等队列中的操作到一定数量或者到了一定时间间隔,浏览器就会flush队列,进行一批处理,这样多次重排,重绘变成一次重排重绘

    减少 reflow/repaint:
    (1)不要一条一条地修改 DOM 的样式。可以先定义好 css 的 class,然后修改 DOM 的 className。

    (2)不要把 DOM 结点的属性值放在一个循环里当成循环里的变量。
    (3)为动画的 HTML 元件使用 fixed 或 absoult 的 position,那么修改他们的 CSS 是不会 reflow 的。
    (4)千万不要使用 table 布局。因为可能很小的一个小改动会造成整个 table 的重新布局。(table及其内部元素除外,它可能需要多次计算才能确定好其在渲染树中节点的属性,通常要花3倍于同等元素的时间。这也是为什么我们要避免使用table做布局的一个原因。)

    (5)不要在布局信息改变的时候做查询(会导致渲染队列强制刷新)

55. 事件循环Event loop

https://www.bilibili.com/video/BV1kf4y1U7Ln/spm_id_from=333.788&vd_source=4ca3ad38fe3439acce886a777d9a94c4

主线程从"任务队列"中读取执行事件,这个过程是循环不断的,这个机制被称为事件循环。此机制具体如下:主 线程会不断从任务队列中按顺序取任务执行,每执行完一个任务都会检查微任务队列是否为空(执行完一个 任务的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有微任务。然后再进入下一个循环去 任务队列中取下一个任务执行。

详细步骤

  1. 选择当前要执行的宏任务队列,选择一个最先进入任务队列的宏任务,如果没有宏任务可以选择,则会 跳转至微任务的执行步骤。

  2. 将事件循环的当前运行宏任务设置为已选择的宏任务。

  3. 运行宏任务。

  4. 将事件循环的当前运行任务设置为null。

  5. 将运行完的宏任务从宏任务队列中移除。

6.微任务步骤:进入为热内检查点。

  1. 更新界面渲染。

  2. 返回第一步。

执行进入microtask检查的的具体步骤如下:

  1. 设置进入microtask检查点的标志为true。

  2. 当事件循环的微任务队列不为空时:选择一个最先进入microtask队列的microtask;设置事件循环的当 前运行任务为已选择的microtask;运行microtask;设置事件循环的当前运行任务为null;将运行结束 的microtask从microtask队列中移除。

  3. 对于相应事件循环的每个环境设置对象(environment settings object),通知它们哪些promise为 rejected。

  4. 清理indexedDB的事务。

  5. 设置进入microtask检查点的标志为false。

需要注意的是:当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件, 然后再去宏任务队列中取出一个 事件。同一次事件循环中, 微任务永远在宏任务之前执行。

56. 浏览器垃圾回收机制

浏览器的 Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。其原理是:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但是这个过程不是实时的,因为其开销比较大并且GC时停止响应其他操作,所以垃圾回收器会按照固定的时间间隔周期性的执行。

不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,而闭包中由于内部函数的原因,外部函数并不能算是结束。

什么时候触发垃圾回收?

垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。IE6的垃圾回收是根据内存分配量运行的,当环境中存在256个变量、4096个对象、64k的字符串任意一种情况的时候就会触发垃圾回收器工作,看起来很科学,不用按一段时间就调用一次,有时候会没必要,这样按需调用不是很好吗?但是如果环境中就是有这么多变量等一直存在,现在脚本如此复杂,很正常,那么结果就是垃圾回收器一直在工作,这样浏览器就没法儿玩儿了。

微软在IE7中做了调整,触发条件不再是固定的,而是动态修改的,初始值和IE6相同,如果垃圾回收器回收的内存分配量低于程序占用内存的15%,说明大部分内存不可被回收,设的垃圾回收触发条件过于敏感,这时候把临街条件翻倍,如果回收的内存高于85%,说明大部分内存早就该清理了,这时候把触发条件置回。这样就使垃圾回收工作职能了很多

57. 顺序存储结构和链式存储结构的比较

优缺点

  1. 顺序存储时,相邻数据元素的存放地址也相邻(逻辑与物理统一);要求内存中可用存储单元的地址必须是连续的。
    • 优点:存储密度大(=1),存储空间利用率高。
    • 缺点:插入或删除元素时不方便。
  2. 链式存储时,相邻数据元素可随意存放,但所占存储空间分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针
    • 优点:插入或删除元素时很方便,使用灵活。
    • 缺点:存储密度小(<1),存储空间利用率低。

使用情况

  • 顺序表适宜于做查找这样的静态操作;
  • 链表宜于做插入、删除这样的动态操作。
  • 若线性表的长度变化不大,且其主要操作是查找,则采用顺序表;
  • 若线性表的长度变化较大,且其主要操作是插入、删除操作,则采用链表

顺序表与链表的比较

  • 基于空间的比较
    • 存储分配的方式
      • 顺序表的存储空间是静态分配的
      • 链表的存储空间是动态分配的
    • 存储密度 = 结点数据本身所占的存储量/结点结构所占的存储总量
      • 顺序表的存储密度 = 1
      • 链表的存储密度 < 1
  • 基于时间的比较
    • 存取方式
      • 顺序表可以随机存取,也可以顺序存取
      • 链表是顺序存取的
    • 插入/删除时移动元素个数
      • 顺序表平均需要移动近一半元素
      • 链表不需要移动元素,只需要修改指针

58. token 能放在cookie中吗

  • token 是在客户端频繁向服务端请求数据,服务端频繁的去数据库查询用户名和密码并进行对比,判断用户名和密码正确与否,并作出相应提示,在这样的背景下,token 便应运而生。
  • 「简单 token 的组成」:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串)

前端面试题--js_第14张图片

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端签发一个 token ,并把它发送给客户端
  4. 客户端接收 token 以后会把它存储起来,比如放在 cookie 里或者 localStorage 里
  5. 客户端每次发送请求时都需要带着服务端签发的 token(把 token 放到 HTTP 的 Header 里)
  6. 服务端收到请求后,需要验证请求里带有的 token ,如验证成功则返回对应的数据

59. 前端性能优化手段

https://juejin.cn/post/7016868803069886471

60. ajax

ajax:异步的javascript和XML

ajax 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术

传统的网页(即不用ajax技术的网页),想要更新内容或者提交一个表单,都需要重新加载整个网页。使用ajax技术的网页,通过在后台服务器进行少量的数据交换,就可以实现异步局部更新。

61. webpack 的 配置

在项目中使用npm run build进行打包

webpack 是一种前端资源构建工具,一个静态模块打包器(module bundler)。

在 webpack 看来, 前端的所有资源文件(js/json/css/img/less/…)都会作为模块处理。

它将根据模块的依赖关系进行静态分析,打包生成对应的静态资源(bundle)

webPack是一个前端模块化方案,侧重模块打包,把开发中的所有资源(图片、js文件、css文件等)都看成模块,通过loader(加载器)和plugins(插件)对资源进行处理,打包成符合生产环境部署的前端资源

1.2 webpack 五个核心概念

1.2.1 Entry

入口(Entry)指示 webpack 以哪个文件为入口起点开始打包,分析构建内部依赖图。

1.2.2 Output

输出(Output)指示 webpack 打包后的资源 bundles 输出到哪里去,以及如何命名。

1.2.3 Loader

Loader 让 webpack 能 够 去 处 理 那 些 非 JavaScript 文 件 (webpack 自 身 只 理 解

JavaScript)

1.2.4 Plugins

插件(Plugins)可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,

一直到重新定义环境中的变量等。

1.2.5 Mode

模式(Mode)指示 webpack 使用相应模式的配置。(development / production)

module.exports = {
	entry: './src/js/index.js', // 入口文件
    output: { // 输出配置
    filename: './built.js', // 输出文件名
    path: resolve(__dirname, 'build/js') // 输出文件路径配置
    },
    mode: 'development' //开发环境   
};

webpack的loader和plugin有什么区别

二者区别:

loader即为文件加载器,操作的是文件,将文件A通过loader转换成文件B,是一个单纯的文件转化过程。

plugin即为插件,是一个扩展器,丰富webpack本身,增强功能 ,针对的是在loader结束之后,webpack打包的整个过程,他并不直接操作文件,而是基于事件机制工作,监听webpack打包过程中的某些节点,执行广泛的任务。

62. git

Git 是一个开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目。

  1. 创建新仓库 git init
  2. 提出更改(添加到暂存区) git add
  3. 实际提交改动:git commit -m “代码提交信息”
  4. 推送到远程仓库:git push origin main

63. 在分支a工作代码还没修改完,这时突然有一个紧急的bug需要你去b分支修复,但是a分支的工作还不能add,应该用什么git指令(git stash)

https://www.cnblogs.com/tocy/p/git-stash-reference.html添加链接描述
综合下网上的介绍和资料,git stash(git储藏)可用于以下情形:

发现有一个类是多余的,想删掉它又担心以后需要查看它的代码,想保存它但又不想增加一个脏的提交。这时就可以考虑git stash
使用git的时候,我们往往使用分支(branch)解决任务切换问题,例如,我们往往会建一个自己的分支去修改和调试代码, 如果别人或者自己发现原有的分支上有个不得不修改的bug,我们往往会把完成一半的代码commit提交到本地仓库,然后切换分支去修改bug,改好之后再切换回来。这样的话往往log上会有大量不必要的记录。其实如果我们不想提交完成一半或者不完善的代码,但是却不得不去修改一个紧急Bug,那么使用git stash就可以将你当前未提交到本地(和服务器)的代码推入到Git的栈中,这时候你的工作区间和上一次提交的内容是完全一样的,所以你可以放心的修Bug,等到修完Bug,提交到服务器上后,再使用git stash apply将以前一半的工作应用回来。
经常有这样的事情发生,当你正在进行项目中某一部分的工作,里面的东西处于一个比较杂乱的状态,而你想转到其他分支上进行一些工作。问题是,你不想提交进行了一半的工作,否则以后你无法回到这个工作点。解决这个问题的办法就是git stash命令。储藏(stash)可以获取你工作目录的中间状态——也就是你修改过的被追踪的文件和暂存的变更——并将它保存到一个未完结变更的堆栈中,随时可以重新应用。

64. es6 中 extends 类继承实现原理

https://blog.csdn.net/terrychinaz/article/details/112426029
ES6 中提供的 class 和 extends 本质上只是语法糖,底层的实现原理依然是构造函数和寄生组合式继承。 所以对于一个合格的前端工程师来说,即使 ES6 已经到来,对于 ES5 中的这些基础原理我们依然需要好好掌握。

65. js往原型中添加方法

https://blog.csdn.net/LttleJoker/article/details/109282792

66. 有哪些异步处理方法?

JavaScript中异步编程的方法有:

  • 回调函数
  • 事件监听
  • 发布/订阅
  • promise
  • generator(ES6)
  • async/await (ES7)

67. async await和promise的区别?

promise是es6推出的,可以解决以前写异步代码使用回调函数的回调地狱的问题,他有三种状态,pending(进行中)、resolved(已完成)、rejected(已失败),只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都不能改变这个状态, Promise构造函数的原型对象上,有then()和catch()等方法,then()第一个参数接收resolved()传来的数据,catch()第一个参数接收rejected()传来的数据,但是promise会形成链式调用,不够美观,

async/await是ES7新特性,async/await是基于Promise实现的,await 操作符用于等待一个 Promise 对象。它只能在异步函数 async function 中使用,async 函数的返回值为 Promise 对象,async 函数返回的 Promise 的结果由函数执行的结果决定,await 必须写在 async 函数中, 但 async 函数中可以没有 await,如果 await 的 Promise 失败了, 就会抛出异常, 需要通过 try…catch 捕获处理。

function fn2() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            // resolve(1000)
            reject(1000)
        }, 1000);
    })
}
async function fn3() {
    try {
        const value = await fn2()
    } catch (error) {
        console.log('得到失败的结果', error)
    }
}
fn3() // 得到失败的结果 1000

68. 登录逻辑

  1. 用户名、密码验证成功后,后端签发 token 返回给前端。
  2. 前端把 token 保存到本地存储
  3. 每次请求前,通过 axios 请求拦截器,统一发送 token
  4. 通过 Vue 导航守卫,和axios 响应拦截器,统一保护页面。

69. 为什么typeof null 结果是object

之前只知道typeof null = object,但是却从来不知道是为什么。最新查阅资料的时候,看到了这个原理,记录下来,方便自己以后查看。

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

这个bug是第一版Javascript留下来的。在这个版本,数值是以32字节存储的,由标志位(1~3个字节)和数值组成。标志位存储的是低位的数据。这里有五种标志位:

  • 000:对象,数据是对象的应用。

  • 1:整型,数据是31位带符号整数。

  • 010:双精度类型,数据是双精度数字。

  • 100:字符串,数据是字符串。

  • 110:布尔类型,数据是布尔值。

    最低位有一位,那么标志位只有一个1字节长度;或者是零位,标志位有3个字节长度,多出两个了字节,一共多出四种类型。

70. 事件冒泡和事件捕获

https://blog.csdn.net/chenjuan1993/article/details/81347590

https://juejin.cn/post/7070363924083769358

冒泡事件:比如说鼠标点击了一个按钮,同样的事件将会在那个元素的所有祖先元素中被触发。这一过程被称为事件冒泡。

<div class="box">
	< button class="btn">按钮</button>
</div>


<script type="text/javascript">
	$('.btn').click(function () {
		alert('按钮被点击了')
	});
	$('.box').click(function () {
		alert('box被点击了')
	})
</script>

当我们点击按钮后,因为按钮也属于.box元素,所以按钮的父元素.box也会触发点击事件

阻止冒泡事件有三种方法:

1.event.stopPropagation()方法

	$('.btn').click(function (even) {
		even.stopPropagation();
		alert('按钮被点击了');
	})

这是阻止事件的冒泡方法,不让事件向documen上蔓延,但是默认事件任然会执行,当你调用这个方法的时候,如果点击一个连接,这个连接仍然会被打开。
例如:

<a href="https://www.csdn.net/" class="box">
	<button class="btn">按钮</button>
</a>

2.event.preventDefault()方法

	$('.btn').click(function (even) {
		even.preventDefault();
		alert('按钮被点击了');
	})

这是阻止默认事件的方法,调用此方法是,连接不会被打开,但是会发生冒泡,冒泡会传递到上一层的父元素;

3.return false ;

$('.btn').click(function (even) {
		alert('按钮被点击了');
		return false;
	})

这个方法比较暴力,他会同事阻止事件冒泡也会阻止默认事件;写上此代码,连接不会被打开,事件也不会传递到上一层的父元素;可以理解为return false就等于同时调用了event.stopPropagation()和event.preventDefault()

71. DOM事件流的三个阶段

  1. 事件捕获阶段
  2. 处于目标阶段
  3. 事件冒泡阶段

72. 实现一个类(es5与es6)手写

  1. ES5中用构造函数实现一个类
//构造函数的写法
      var Person = function (name, age) {
      //这里定义的是实例属性
        this.name = name;
        this.age = age;
      //这里定义的是实例方法
        this.sayHello = function () {
      console.log("我是实例方法" + this.name);
    };
   	 };
   };
      //创建实例对象
      let p1 = new Person("小米", 18);
      console.log(p1);
      // 添加原型链属性/方法(所有实例共享)
      Person.prototype.Way2 = function () {
        console.log(`我的名字是${this.name},今年${this.age}`);
      };
      //访问原型链方法
      p1.Way2();
      // 静态方法/静态属性(只能由构造函数本身访问)
      Person.title = "这是构造函数静态属性";
      console.log(Person.title);
      console.log(p1.title); //undefined
      //动态创建实例方法(与原型链方法重名)
      p1.Way2 = function () {
        console.log("我是p1的sayHello()");
      };
      //优先从实例方法中读取,不会去读取原型链方法
      p1.Way2();//我是p1的sayHello()

  1. ES6-class类

ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
类就是将原先写在构造函数内部的实例属性和方法代码,写入到constructor()中
将原先写在原型链prototype上面的属性直接写在类里面
将静态属性直接写在类里面并且在前面添加static关键字
类的数据类型就是函数,类本身就指向构造函数

//类的写法
      class Superman {
      //constructor函数内部定义的是实例属性和方法
        constructor(name, age) {
          //这里定义的是实例属性
          this.name = name;
          this.age = age;
          //这里定义的是实例方法
        this.sayHello = function () {
      console.log("我是实例方法" + this.name);
    };
   }
    // 定义的是原型链(prototype)方法
        sayHello() {
          console.log(`我的名字是${this.name},今年${this.age}`);
        }
        // 静态方法(只能由类本身访问);调用方法:Superman.foo()
        static foo() {
          console.log("我是类的静态方法");
        }
        // 静态属性(只能由类本身访问);调用方法:Superman.title
        static title = "这是类的标题";
      }
      //创建实例
      var s1 = new Superman("张三", 20);
      console.log(s1);
      console.log(Superman);
      console.dir(Superman);
      // 类的本质是函数
      console.log(typeof Superman); //function
      console.log(s1.sayHello === Superman.prototype.sayHello); //true

前端面试题--js_第15张图片

72. 后台管理系统中的权限管理是怎么实现的?

登录:当用户填写完账号和密码后向服务端验证是否正确,验证通过之后,服务端会返回一个token

拿到token之后(我会将这个token存贮到cookie中,保证刷新页面后能记住用户登录状态),前端会

根据token再去拉取一个 user_info 的接口来获取用户的详细信息(如用户权限,用户名等等信息)。

权限验证:通过token获取用户对应的 权限,动态根据用户的 权限算出其对应有权限的路由,通过

router.addRoutes 动态挂载这些路由。

具体思路:

黑马程序员登录成功后,服务端会返回一个 token(该token的是一个能唯一标示用户身份的一个key),之后我

们将token存储在本地cookie之中,这样下次打开页面或者刷新页面的时候能记住用户的登录状态,不

用再去登录页面重新登录了。

ps:为了保证安全性,我司现在后台所有token有效期(Expires/Max-Age)都是Session,就是当浏览器关

闭了就丢失了。重新打开游览器都需要重新登录验证,后端也会在每周固定一个时间点重新刷新

token,让后台用户全部重新登录一次,确保后台用户不会因为电脑遗失或者其它原因被人随意使用账

号。

用户登录成功之后,我们会在全局钩子 router.beforeEach 中拦截路由,判断是否已获得token,在

获得token之后我们就要去获取用户的基本信息了

页面会先从 cookie 中查看是否存有 token,没有,就走一遍上一部分的流程重新登录,如果有token,

就会把这个 token 返给后端去拉取user_info,保证用户信息是最新的。 当然如果是做了单点登录得功

能的话,用户信息存储在本地也是可以的。当你一台电脑登录时,另一台会被提下线,所以总会重新登

录获取最新的内容。

先说一说我权限控制的主体思路,前端会有一份路由表,它表示了每一个路由可访问的权限。当用户登

录之后,通过 token 获取用户的 role ,动态根据用户的 role 算出其对应有权限的路由,再通过

router.addRoutes 动态挂载路由。但这些控制都只是页面级的,说白了前端再怎么做权限控制都不是

绝对安全的,后端的权限验证是逃不掉的。

我司现在就是前端来控制页面级的权限,不同权限的用户显示不同的侧边栏和限制其所能进入的页面(也

做了少许按钮级别的权限控制),后端则会验证每一个涉及请求的操作,验证其是否有该操作的权限,每

一个后台的请求不管是 get 还是 post 都会让前端在请求 header 里面携带用户的 token,后端会根据

token 来验证用户是否有权限执行该操作。若没有权限则抛出一个对应的状态码,前端检测到该状

态码,做出相对应的操作。

使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件。

具体实现:

创建vue实例的时候将vue-router挂载,但这个时候vue-router挂载一些登录或者不用权限的公用的页

面。

当用户登录后,获取用role,将role和路由表每个页面的需要的权限作比较,生成最终用户可访问的路

由表。

调用router.addRoutes(store.getters.addRouters)添加用户可访问的路由。

使用vuex管理路由表,根据vuex中可访问的路由渲染侧边栏组件。

73. 知道lodash吗?它有哪些常见的API ?

Lodash是一个一致性、模块化、高性能的 JavaScript 实用工具库。

_.cloneDeep 深度拷贝

_.reject 根据条件去除某个元素。

_.drop(array, [n=1] ) 作用:将 array 中的前 n 个元素去掉,然后返回剩余的部分.

74. 优化上传很大的文件

不管怎样简单的需求,在量级达到一定层次时,都会变得异常复杂

文件上传简单,文件变大就复杂

上传大文件时,以下几个变量会影响我们的用户体验

  • 服务器处理数据的能力
  • 请求超时
  • 网络波动

上传时间会变长,高频次文件上传失败,失败后又需要重新上传等等

为了解决上述问题,我们需要对大文件上传单独处理

这里涉及到分片上传断点续传两个概念

分片上传

分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(Part)来进行分片上传

如下图

前端面试题--js_第16张图片

上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件

大致流程如下:

  1. 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;
  2. 初始化一个分片上传任务,返回本次分片上传唯一标识;
  3. 按照一定的策略(串行或并行)发送各个分片数据块;
  4. 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件

断点续传

断点续传指的是在下载或上传时,将下载或上传任务人为的划分为几个部分

每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度

一般实现方式有两种:

  • 服务器端返回,告知从哪开始
  • 浏览器端自行处理

上传过程中将文件在服务器写为临时文件,等全部写完了(文件上传完),将此临时文件重命名为正式文件即可

如果中途上传中断过,下次上传的时候根据当前临时文件大小,作为在客户端读取文件的偏移量,从此位置继续读取文件数据块,上传到服务器从此偏移量继续写入文件即可

二、实现思路

整体思路比较简单,拿到文件,保存文件唯一性标识,切割文件,分段上传,每次上传一段,根据唯一性标识判断文件上传进度,直到文件的全部片段上传完毕

前端面试题--js_第17张图片

三、使用场景
  • 大文件加速上传:当文件大小超过预期大小时,使用分片上传可实现并行上传多个 Part, 以加快上传速度
  • 网络环境较差:建议使用分片上传。当出现上传失败的时候,仅需重传失败的Part
  • 流式上传:可以在需要上传的文件大小还不确定的情况下开始上传。这种场景在视频监控等行业应用中比较常见

75. localstorage 的跨域存储方案

详情见:https://www.jianshu.com/p/e86d92aeae69
另外,不同浏览器无法共享localStorage和sessionStorage中的信息同一浏览器的相同域名和端口的不同页面间可以共享相同的 localStorage,但是不同页面间无法共享sessionStorage的信息。这里需要注意的是,页面仅指顶级窗口,如果一个页面包含多个iframe且他们属于同源页面,那么他们之间是可以共享sessionStorage的。在实际开发过程中,遇到的最多的问题就是localStorage的同源策略问题。为了了解这个问题,我们先得清楚什么是同源策略。同源策略(same-origin policy)是浏览器执行的一种安全措施,目的是为了保证用户信息的安全,防止恶意的网站窃取数据。

只要不同源就不能共享localStorage的数据。但是在实际开发中又时常会遇到这样的需求,那我们该如何解决呢?

目前广泛采用的是postMessageiframe相结合的方法。postMessage(data,origin)方法允许来自不同源的脚本采用异步方式进行通信,可以实现跨文本档、多窗口、跨域消息传递。接受两个参数:

  • data:要传递的数据,HTML5规范中提到该参数可以是JavaScript的任意基本类型或可复制的对象,然而并不是所有浏览器支持任意类型的参数,部分浏览器只能处理字符串参数,所以在传递参数时需要使用JSON.stringify()方法对对象参数序列化。
  • origin:字符串参数,指明目标窗口的源,协议+主机+端口号[+URL],URL会被忽略,所以可以不写,只是为了安全考虑,postMessage()方法只会将message传递给指定窗口,当然也可以将参数设置为"*“,这样可以传递给任意窗口,如果要指定和当前窗口同源的话设置为”/"。

76. 怎么删除、添加、获取localstorage里面的数据

localStorage很多时候用来存储数据非常好,方便在页面中使用某些数据的时候调用

首先,通过setItem(key,value)即可存储数据
然后用getItem(key)的方式即可取得数据

前端面试题--js_第18张图片

可以看到,设置之后在浏览器的localStorage中看到存储的数据

前端面试题--js_第19张图片

使用localStoage存储的数据除非用户手动清空浏览器的信息,否则不会被删除
当然我们也可以通过removeItem(key)的形式来移除
removeItem会删除对应key的内容

前端面试题--js_第20张图片

如果想要删除全部的数据可以用clear方法来清除

前端面试题--js_第21张图片

77. for in 循环会遍历原型链上的属性的问题

1.使用 for in 循环遍历对象的属性时,原型链上的所有属性都将被访问:

Object.prototype.say="cgl";   // 修改Object.prototype  
    var person ={ age: 18 };
    for (var key in person) {
        console.log(key, person[key]);//这里用person.key得不到对象key的值,用person[key] 或者 eval("person."+key);
    }   //结果: age 18 
                say cgl

2.只遍历对象自身的属性,而不遍历继承于原型链上的属性,使用hasOwnProperty 方法过滤一下。

Object.prototype.say="cgl";
    var person ={
        age: 18
    };
    for (var key in person) {
        if(person.hasOwnProperty(key)){
            console.log(key, eval("person."+key));
        }
    }  

二.Object.keys()

Object.keys() 方法会返回一个由给定对象的自身可枚举属性组成的数组,数组中属性名的排列顺序和使用 for…in 循环遍历该对象时返回的顺序一致 (两者的主要区别是 一个 for-in 循环还会枚举其原型链上的属性)。返回值是这个对象的所有可枚举属性组成的字符串数组。

Object.prototype.say="cgl";
var person ={ age: 18};
console.log(Object.keys(person));//结果  ["age"] 

小技巧:object对象没有length属性,可以通过Object.keys(person).length,来获取person的长度了。

78. settimeout延时3秒所经历的时间一定是3秒吗?

不是
setTimeou中的方法是异步任务,会被放到任务队列中,需要等执行栈中的同步任务执行完之后,任务队列中的任务才能执行,所以需要等到同步任务执行完才开始计时,因此一定会大于等于3s。

79. 为什么设计时要区分宏任务、微任务

为什么要分微任务和宏任务?

微任务是线程之间的切换,速度快。不用进行上下文切换,可以快速的一次性做完所有的微任务。

宏任务是进程之间的切换,速度慢,且每次执行需要切换上下文。因此一个Eventloop中只执行一个宏任务。

而区分微任务和宏任务的根本原因是为了插队。由于微任务执行快,一次性可以执行很多个,在当前宏任务执行后立刻清空微任务可以达到伪同步的效果,这对视图渲染效果起到至关重要的作用。

反观如果不区分微任务和宏任务,那么新注册的任务不得不等到下一个宏任务结束后,才能执行。

80. 对nodejs里的comonjs的理解?对模块化规范的理解?

81. 哪些方法可以判断是一个数组 typeof 数组 返回什么

首先: let arr = [];

1.通过instanceof运算符判断

从构造函数入手:可以判断一个对象是否是在其原型链上原型构造函数中的属性。

console.log(arr instanceof Array);   //true
复制代码

再说说 typeof 和 instanceof 的区别? 两者都可以用来判断变量,typeof会返回基本类型,而instanceof只会返回一个布尔值。

2.通过constructor判断

这个属性是返回对象相对应的构造函数,Object的每个实例都有构造函数 constructor,用于保存着用于创建当前对象的函数。

console.log(arr.constructor === Array);   //true
复制代码

3.通过数组自带的isArray方法判断

ES5中新增了Array.isArray方法,IE8及以下不支持,用于确定传递的值是否是一个 Array。

console.log(Array.isArray(arr));   //true
复制代码

4.通过isPrototypeOf()方法判断

从原型入手,Array.prototype 属性表示 Array 构造函数的原型,

其中有一个方法是 isPrototypeOf() 用于测试一个对象是否存在于另一个对象的原型链上。

console.log(Array.prototype.isPrototypeOf(arr));   //true

5.通过Object.getPrototypeOf方法判断

Object.getPrototypeOf() 方法返回指定对象的原型,所以只要跟Array的原型比较即可。

console.log(Object.getPrototypeOf(arr) === Array.prototype); // true
复制代码

6.通过Object.prototype.toString.call()判断

虽然Array也继承自Object,但js在Array.prototype上重写了toString,而我们通过toString.call(arr)实际上是通过原型链调用了。可以获取到变量的不同类型。

console.log(Object.prototype.toString.call(arr) === '[object Array]');   //true

console.log(Object.prototype.toString.call(arr).slice(8,-1)=== 'Array');   //true

console.log(Object.prototype.toString.call(arr).indexOf('Array') !== -1);    //true

作者:未i
链接:https://juejin.cn/post/6976089271841226783
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

32. 什么是预加载

1.什么是预加载
资源预加载是另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被使用到。预加载简单来说就是将所有所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。

2.为什么要用预加载
在网页全部加载之前,对一些主要内容进行加载,以提供给用户更好的体验,减少等待的时间。否则,如果一个页面的内容过于庞大,没有使用预加载技术的页面就会长时间的展现为一片空白,直到所有内容加载完毕。

3.实现预加载的几种办法
https://blog.csdn.net/u011215669/article/details/80526530

你可能感兴趣的:(前端,javascript)