JS 高频面试题汇总

# 说一下JS 中的数据类型有哪些

JS 数据类型包括 基本 / 引用 / 特殊 数据类型:

  1. 基本数据类型:StringNumberBoolean
  2. 引用数据类型:ObjectArrayFunction
  3. 特殊数据类型:NullUndefined
  4. 原始数据类型 Symbol (ES6)
    独一无二的值,即 Symbol('1’) != Symbol('1’)

# 追问:判断 JS 数据类型有几种方法

常用的有 typeof、instanceof
不常用的有 constructor、 prototype / toString

  1. typeof 是个一元运算,放在任意类型的运算数之前,返回一个 字符串 说明运算数的类型。
    可检测出的类型有:
    'number''string''boolean''object'
    'undefined''function''symbol'
    其中对象"object" 包括:ObjectArraynew RegExp()new Date()Null 特殊类型
    缺点:判断普通类型没有问题,但不能准确判断 引用数据类型
  2. instanceof 运算符用来检测一个对象在其原型链中是否存在一个构造函数的 prototype 属性
    通俗讲 instanceof 检测的是原型,检测左边的对象是否是右边类的实例
 [] instanceof Array ==> true

注意:instanceof 能够判断出 []Array 的实例,也是 Object 的实例
因为 [].__proto__ 指向 Array.prototype,而 Array.prototype.__proto__ 又指向了 Object.prototype,最终 Object.prototype.__proto__ 指向了 null 原型链结束。
类似的还有 new Date()new Error()new 自定义类()
归纳:所有对象都是 Object 的实例 或 Object是一切对象的父对象

  1. 根据对象的 constructor 判断
    原理:每个构造函数都有一个 constructor 属,指回它本身
[].coconstructor === Array ==> true

判断 数字、字符串、函数 和 日期时,必须得用关键字 new 创建才行
因为只有构造函数才有 constructor 属性,还有两点需要注意:

  1. nullundefined 是无效的对象,因此不会有 constructor 存在,
  2. 函数的 constructor 是不稳定的,当重写 prototype 后,
    原有的 constructor 引用会丢失,constructor 会默认为 Object
  1. 使用 toString 判断
    toString()Object 的原型方法,该方法默认返回当前对象的 [[Class]] 。
    这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
    对于 Object 对象,直接调用 toString() 就能返回 [object Object]
    而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。
Object.prototype.toString.call(undefined) ===  '[object Undefined]'
Object.prototype.toString.call(null) ===  '[object Null]'
Object.prototype.toString.call(123) === '[object Number]'

'[object Undefined]','[object Null]', '[object Number]','[object String]','[object Boolean]','[object Array]',
'[object Function]','[object Date]', '[object RegExp]','[object Error]'

  1. JQuery 提供的 jquery.type()
    返回说明操作数的字符串
jQuery.type(123) === "number"
jQuery.type(undefined) === "undefined"
jQuery.type(null ) === "null "
Query.type(new Date()) === "date"
jQuery.type(new Error()) === "error"

'undefined''null','date''error'
'array'``'boolean''number''string'
'function''regexp'

# 追问:null 和 undefined 有啥区别?

null:是 Null 类型,表示一个 空对象指针尚未存在的对象
即该处不应该有值,使用 typeof 运算得到 'object' ,是个特殊对象值,转为数值为 0
也可以理解是表示程序级的、正常的或在意料之中的值的空缺

  1. 作为函数的参数,表示该函数的参数不是对象
  2. 作为对象原型链的终点

注意:null 不是一个对象,但 typeof null === object 原因是不同的对象在底层都会表示为二进制,在 JS 中如果二进制的前三位都为 0,就会被判断为 object 类型,null 的二进制全为 0,自然前三位也是 0,所以 typeof null === 'objcet'

undefined:是 Undefined 类型,表示一个 无 的原始值缺少值
即此处应该有一个值,但还没有定义,使用 typeof undefined === 'undefined',转为数值为 NaN
它是在 ECMAScript 第三版引入的预定义全局变量,为了区分 空指针对象未初始化的变量
也可以理解是表示系统级的、出乎意料的或类似错误的值的空缺

  1. 变量被声但没有赋值时
  2. 调用函数时,应该提供的参数没有提供时
  3. 对象没有赋值的属性时,属性值为 undefined
  4. 函数没有返回值时,默认返回值为 undefined

# 追问: JS 有哪些内置对象

数据封装类对象:Object、Array、Boolean、Number、String
其他对象:Function、Arguments、Math、Date、RegExp、Error

# 追问:说说你对原型和原型链的理解

原型: 每一个构造函数都会自动带一个 prototype 属性,是个指针,指向一个对象,就是 原型对象
原型对象 上默认有一个属性 constructor ,也是个指针,指向构造函数本身。

  • 优点:原型对象上所有的 属性方法 都能被构造函数的 实例对象 共享访问。
  • 缺点:多个实例对引用类型的操作会被篡改。
    因为每次实例化,引用类型的数据都指向同一个地址,所以它们 读/写 的是同一个数据,当一个实例对其进行操作,其他实例的数据就会一起更改( 这也是 Vue 中 data 为什么是一个函数的原因 )。

原型链: 每个实例对象都有一个原型 __proto__,这个原型还可以有它自己的原型,以此类推,形成一个链式结构即原型链。

每个构造函数都有一个原型对象 prototype,原型对象上包含一个指向构造函数的指针 constructor
而每个实例都包含着一个指向原型对象的内部指针 __proto__
可以通过内部指针 __proto__ 访问到原型对象,原型对象通过 constructor 找到构造函数。

如果 A对象 在 B 对象的原型链上,可以说它们是 B对象继承了 A对象。

原型链作用:如果试图访问对象的某个属性,会首先在 对象内部 寻找该属性,直至找不到,然后才在该对象的原型里去找这个属性,以此类推。

new 关键字创建一个实例都做了什么?

  1. 像普通对象一样,形成自己的私有作用域( 形参赋值,变量提升 )
  2. 创建一个新对象,将 this 指向这个新对象( 构造函数的作用域赋给新对象 )
  3. 执行构造函数中的代码,为这个新对象添加属性、方法
  4. 返回这个新对象( 新对象为构造函数的实例 )

手写一个 new 原理如下:

function myNew(fn, ...arg){
    // 创建一个对象,让它的原型链指向 fn.prototype
    
    // 普通方法
    // let obj = {};
    // obj.__proto__ = fn.prototype;
    
    // 使用 Object.create([A对象]):创建一个空对象 obj,并让 obj.__proto__ 等于 A对象
    let obj = Object.create(fn.prototype);

    fn.call(obj, ...arg);
    return obj;
}

可以用 instanceof 测试构造函数的 prototype 属性是否出现在实例对象的原型链中
也可以用 obj.hasOwnProperty(prop)测试对象自身属性中是否具有指定的属性

# 追问:刚刚提到继承,说说继承的几种方式

1. 原型链继承
让新实例的原型等于父类的实例

  • 缺点:
    1、新实例无法向父类构造函数传参。
    2、继承单一, 且得手动修正 constructor 指向。
    3、所有新实例都会共享父类实例的属性。
    4、原型上属性是共享的,一个实例修改了原型属性,另一个实例原型属性也会被修改
  • 特点:
    实例可继承:所属构造函数属性、父类构造函数属性、父类原型属性
    注意:实例不能继承父类实例的属性

2. 构造函数继承
callapply 将父类构造函数引入子类函数
即:在子类函数中做了父类函数的自执行

  • 缺点:
    1、只能继承父类构造函数的属性和方法。
    2、无法实现构造函数的复用。
    3、每个新实例都有父类构造函数的副本,很臃肿。
  • 特点:
    1、只继承了父类构造函数的属性,没有继承父类原型的属性。
    2、解决了原型链继承缺点1、2、3。
    3、可以继承多个构造函数属性。
    4、在子实例中可向父实例传参。

3. 组合继承
结合了 原型链继承构造函数继承 两种模式的优点:传参 和 复用

  • 缺点:
    调用两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
  • 特点:
    1、可继承父类原型上的属性,可传参,可复用
    2、每个新实例引入的构造函数属性是私有的

4. 寄生式继承
就是给原型式继承外面套了个壳子。

  • 缺点:
    没用到原型,无法复用。
  • 特点:
    没有创建自定义类型,因为只是套了个壳子返回对象,这个函数就成了创建的新对象。

5. 组合寄生式继承
修复了组合继承的问题 ( 推荐使用 )

6. extends 继承
ES6 中有关 class 的继承方式,引入了 extends 关键字。
但其本质仍然是 构造函数 + 原型链的 组合式继承。

# 说说 JS 中的面向对象

一切事物皆对象,封装、继承、多态。

# 追问:call / apply / bind 有啥区别

都是替换函数中不想要的 this
callapply 是临时的且立即执行,
bind 是永久绑定不立即执行,返回一个新函数,需要时再去执行这个新函数。

call: call( thisObj, obj1, obj2... )
要求传入函数的参数必须单独传入

apply: apply(t hisObj, [argArray] )
要求传入函数的参数必须放入数组中整体传入
apply会将数组打散为单个参数值分别传入

bind: 永久绑定函数中的 this,作用如下:

  1. 创建一个和原函数功能完全一样的新函数.
  2. 将新函数中的 this 永久绑定为指定对象
  3. 将新函数中的部分固定参数提前永久绑定

# 说说 ES6、ES7、ES8 的新特性

ES6的特性:

  1. 类(class)
  2. 模块化(Module)导出(export)导入(import)
  3. 箭头(Arrow)函数
  4. 函数参数默认值
  5. 模板字符串
  6. 延展操作符(Spread operator) 和 剩余运算符(rest operator)
  7. ES6中允许我们在设置一个对象的属性的时候不指定属性名
  8. Promise 异步编程的解决方案
  9. 支持 letconst 块级作用域

ES7的特性

  1. includes() 函数用来判断一个数组是否包含一个指定的值,返回 true / false
  2. 指数操作符在ES7中引入了指数运算符,具有与Math.pow(..)等效的计算结果

ES8的特性

  1. 加入了对 async/await 的支持,也就我们所说的异步函数
  2. Object.values() 是一个与 Object.keys() 类似的新函数,但返回的是 Object 自身属性的所有值,不包括继承的值
  3. Object.entries() 函数返回一个给定对象自身可枚举属性的键值对的数组
  4. String.padStart(targetLength,[padString]) 和 String.padEnd(targetLength,padString])
    5.Object.getOwnPropertyDescriptors() 函数用来获取一个对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。

# require 和 import 区别

importrequire都是被模块化所使用。

  1. 遵循规范
    requireAMD规范引入方式
    importes6的语法标准,如要兼容浏览器的话必须转化成es5的语法
  2. 调用时间
    require是运行时调用,所以require理论上可以运用在代码的任何地方
    import是编译时调用,所以必须放在文件开头
  3. 本质
    require是赋值过程,其实require的结果就是对象、数字、字符串、函数等,再把require的结果赋值给某个变量
    import是解构过程,但是目前所有的引擎都还没有实现import,我们使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import语法会被转码为require
  4. 性能
    require的性能相对于import稍低,因为require是在运行时才引入模块并且还赋值给某个变量
    import只需要依据import中的接口在编译时引入指定模块所以性能稍高

# 追问:Es6 Module 和 Common.js 的区别

CommonJS

  1. 对于基本数据类型,属于复制,会被模块缓存。可在另一个模块可以对该模块输出的变量重新赋值。
  2. 对于复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
  3. 当使用 require 命令加载某个模块时,就会运行整个模块的代码。
  4. common.js 同一个模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载就返回第一次运行的结果,除非手动清除系统缓存。
  5. 循环加载时,属于加载时执行。即脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被 "循环加载",就只输出已经执行的部分,还未执行的部分不会输出。

ES6 Module 模块

  1. ES6 模块中的值属于动态只读引用。
    只读:不允许修改引入变量的值,import 的变量是只读的( 包括 基本/复杂 数据类型 )。当模块遇到 import 命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
    动态:原始值发生变化,import 加载的值也会发生变化( 包括 基本/复杂 数据类型)。
  2. 循环加载时,ES6 模块是动态引用( 只要两个模块之间存在某个引用,代码就能执行 )。

综上:

  1. common.js 是 module.exports / exports 导出,require 导入;ES6 则是 export 导出,import 导入。
  2. common.js 是运行时加载模块,ES6 是在静态编译期间就确定模块的依赖。
  3. ES6 在编译期间会将所有 import 提升到顶部,common.js 不会提升 require。
  4. common.js 导出的是一个值拷贝,会对加载结果进行缓存,一旦内部再修改这个值,则不会同步到外部。ES6 是导出的一个引用,内部修改可以同步到外部。
  5. 两者的循环导入的实现原理不同,common.js 是当模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。ES6 模块是动态引用,如果使用 import 从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用。
  6. common.js 中顶层的 this 指向这个模块本身,而 ES6 中顶层 this 指向 undefined

# 说说闭包

闭包:有权限访问其它函数作用域内变量的函数。

JS 中,变量分为全局变量和局部变量,局部变量的作用域属于函数作用域,在函数执行完以后作用域就会被销毁,内存也会被回收。但是闭包是建立在函数内部的子函数,可访问上级作用域,所以上级函数执行完,作用域也不会被销毁。

闭包解决了什么?

  1. 闭包就是将 函数内部 和 外部 连接起来的桥梁。
    由于闭包可以缓存上级作用域,这样函数外部就可以访问到函数内部的变量。
  2. 起到保护全局不受污染又能 隐藏变量 的作用。

应用:防抖 与 节流
函数去抖(debounce):当调用函数 n 秒后,才会执行该动作,若在这 n 秒内又调用该函数则取消前一次并重新计算执行时间(频繁触发的情况下,只有足够的空闲时间,才执行代码一次)

function debounce(func, wait) {
    let timeout = null
    return function () {
        let context = this
        let args = arguments
        if (timeout) clearTimeout(timeout)
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, wait)
    }
}

函数节流(throttle):函数节流的基本思想是函数预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期(一定时间内 js 方法只跑一次。比如人的眨眼睛,就是一定时间内眨一次)

function throttle(func, wait) {
    let timeout = null
    return function () {
        let context = this
        let args = arguments
        if (!timeout) {
            timeout = setTimeout(() => {
                timeout = null
                func.apply(context, args)
            }, wait)
        }
    }
}

# 箭头函数与普通函数的区别

  1. 箭头函数比普通函数语法更加简洁。
  2. 箭头函数没有自己的 this,它里面的 this 是继承函数所处上下文的 this ( 使用 call/apply/bind 等任何方法都无法改变其指向)。
  3. 箭头函数没有 Arguments (类数组),只能用 ...args 获取传递的参数结合( 数组 )。
  4. 箭头函数是匿名函数,不能使用 new ( 因为没有 thisprototype 属性 )。

# 事件委托是什么,原理是什么

事件委托: 利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
原理:利用事件的 冒泡原理
事件冒泡:就是事件从最深的节点开始,然后逐步向上传播事件。

作用:

  1. 提高性能:每一个函数都会占用内存空间,只需添加一个事件处理程序代理所有事件,所占用的内存空间更少;
  2. 动态监听:使用事件委托可以自动绑定动态添加的元素,即新增的节点不需要主动添加也可以具有和其它元素一样的事件。

如何 阻止冒泡 和 默认事件

停止冒泡: 
window.event ? window.event.cancelBubble = true : e.stopPropagation();
阻止默认事件: 
window.event ? window.event.returnValue = false : e.preventDefault();

# 追问:说前端中的事件流

事件发生时在元素节点之间按照特定的顺序传播的过程叫做 DOM事件流
共分为三大阶段:

捕获阶段(事件从 Document 节点 自上而下 向目标节点传播的阶段)
目标阶段(真正的目标节点正在处理事件的阶段)
冒泡阶段(事件从目标节点 自下而上 向 Document 节点传播的阶段)

事件冒泡:从事件源逐级向上传播到 DOM 最顶层节点的过程。
事件捕获:从 DOM 最顶层节点逐级向下传播到事件源的过程。

# 追问:说说事件队列

JavaScript 语言的一大特点就是 单线程,同一个时间只能做一件事。

作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是 单线程,否则会带来很复杂的同步问题。比如 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript单线程 的本质。

任务队列的本质

  1. 所有 同步任务 都在 主线程 上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还有一个 任务队列(task queue)。
    只要 异步任务 有了运行结果,就在 任务队列 之中放置一个事件。
  3. 执行栈 中的所有同步任务执行完毕,系统就会读取 任务队列,看看里面有哪些事件。
    哪些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

主线程(执行栈)和 任务队列 先进先出 的通信称为 事件循环( Event Loop )
主要分为:
宏任务(macro-task):DOM事件绑定,定时器,Ajax回调
微任务(micro-task):Promise,MutationObserver (html5新特性)
事件循环机制:主线程 =>所有微任务 ->宏任务
先进先执行,如果里面有微任务,则下一步先执行微任务,否则继续执行宏任务

setTimeout()
将事件插入到了事件队列,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。
当主线程时间执行过长,无法保证回调会在事件指定的时间执行。
浏览器端每次 setTimeout 会有 4ms 的延迟,当连续执行多个 setTimeout,有可能会阻塞进程,造成性能问题。
setImmediate()
事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行。和 setTimeout(fn,0) 的效果差不多。

# 追问:说说堆栈

栈内存 一般储存 基础数据类型,遵循 先进后出后进先出 的原则,大小固定并由系统自动分配内存空间,运行效率高,有序存储
中的 DOM render,ajax,setTimeout,setInterval 会依次进入到队列中,当栈中代码执行完毕后,再将队列中的事件放到执行栈中依次执行

堆内存 一般储存 引用数据类型JavaScript 不允许直接访问 堆内存 中的位置,需要从 栈中 获取该对象的 地址引用/指针,再从 堆内存 中获取数据。存储值大小不定,可动态调整,主要用来存放对象。空间大,但是运行效率相对较低,无序存储,可根据引用直接获取。

# 说下代码执行结果

let obj = {}, a = 0, b = '0';
obj[a] = 123;
obj[b] = 456;
console.log(obj); // {0: 456} 

对象存在 堆 中,数字属性 和 字符串属性相等
let obj = {}, a = Symbol(1), b = Symbol(1);
obj[a] = 123;
obj[b] = 456;
console.log(obj); // {Symbol(1): 123, Symbol(1): 456}

Symbol 表示独一无二的值,即 Symbol(1) != Symbol(1)
let obj = {}, a = {name: '张三'}, b = {name: '李四'};
obj[a] = 123;
obj[b] = 456;
console.log(obj); // {[object Object]: 456}

把对象作为另一个对象的属性时,会 调用 toString 转换为字符串

# 追问:对象和数组有啥区别

对象:是包含已命名的值的无序集合,也被称为 关联数组
数组:是包含已编码的值的有序集合

  1. 创建方式不同,数组是[] / new Array,对象是{} / new Object
  2. 调用方式不同,数组是 arr[下标],对象是 obj.加属性名 / [属性名]
  3. 数组是有序数据的集合,对象是无序。
  4. 数组的数据没有名称,只有下标,对象的数据需要指定名称。
  5. 数组的元素可以重复,对象的属性是唯一的。
  6. 数组的有长度,而对象没有。

# 追问:数组常用的操作方法有哪些

操作数组:push,splice,join,concat
遍历数组:map,forEach,reduce
筛选数组:filter,some,find,findIndex

# 追问:如何快速合并两个数组?

(a). arrA.concat(arrB)
(b). Array.prototype.push.apply(arrA,arrB);
(c). Array.prototype.concat.apply(arrA,arrB);
(d). Array.prototype.concat.call(arrA,arrB);
(e). 数组转成字符串拼接在切割成数组, 或者是循环其中一个数组等...
性能自测对比:
Array.prototype.concat.call > Array.prototype.concat.apply > concat > Array.prototype.push.apply

# 追问:map 和 forEach 有何区别

相同点:

  1. 都是循环遍历数组中的每一项
  2. forEach 和 map方法里每次执行匿名函数都支持3个参数,
    参数分别是item(当前每一项),index(索引值),arr(原数组)
  3. 匿名函数中的 this 都是指向 window( 在 Vue 中指向 Vue 实例)

不同点:

  1. map() 返回一个新数组,原数组不会改变,可链式调用
  2. forEach() 返回值为 undefined,可链式调用

场景:

如只是单纯的遍历可用 forEach()
如操作原数组得到新数组可用 map()

# 追问:filter 和 reduce的区别

filter :筛选出原数组中符合条件的元素组成新数组,原数组不变

var subArr = arr.filter(function(val,i,arr){
    return 判断条件
})

reduce:将数组中每个元素的值,汇总成一个最终结果

var result=arr.reduce(function(prev,val,i,arr){
    return prev+val;//累加
},base);

# 追问:什么是数组扁平化,实现扁平化的方法有哪些?

数组扁平化:一个多维数组变为一维数组,方法如下:

  1. flat( ES 6)
    flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
let newArray = arr.flat([depth]);
depth值可选: 指定要提取嵌套数组的结构深度,默认值为 1,不确定层级也可写 `Infinity`。
  1. reduce
function flatten(arr) {  
    return arr.reduce((result, item)=> {
        return result.concat(Array.isArray(item) ? flatten(item) : item);
    }, []);
}
  1. toString & split
function flatten(arr) {
    return arr.toString().split(',').map(function(item) {
        return Number(item);
    })
} 
  1. join & split
function flatten(arr) {
    return arr.join(',').split(',').map(function(item) {
        return parseInt(item);
    })
}
  1. 扩展运算符
[].concat(...[1, 2, 3, [4, 5]]);  // [1, 2, 3, 4, 5]

也可以做一个遍历,若 arr 中含有数组则使用一次扩展运算符,直至没有为止,如下:
扩展运算符每次只能展开一层数组

function flatten(arr) {
    while(arr.some(item=>Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}
  1. 递归
function flatten(arr) { 
  var res = [];
  arr.map(item => {
    if(Array.isArray(item)) {
      res = res.concat(flatten(item));
    } else {
      res.push(item);
    }
  }); 
  return res;
}

# XSS 跨站脚本攻击 与 CSRF 跨站请求伪造

XSS 攻击原理:攻击者往 Web 页面里插入恶意的脚本代码(css 代码、Javascript 代码等),当用户浏览该页面时,嵌入其中的脚本代码会被执行,从而达到恶意攻击用户的目的,如盗取用户 cookie、破坏页面结构、重定向到其他网站等。

预防

1、将 httpOnly 属性设置为 true,这样使用 js 就获取不到 cookie 了
2、永远不信任用户输入的数据,对输入数据进行验证
3、对发送的数据进行编码转义处理,或者使用正则替换
4、服务端也要做判断处理查看是否有 XSS 攻击,然后做转义处理

CSRF 攻击原理:通过 HTML 标签请求跨域,并用某种手段骗取目标用户认证状态( 例如cookie )信息后进行跨域请求,达到伪造请求的目的 (即用户登录 A 网站,并生成 Cookie,在不登出的情况下访问危险网站 B)。
攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来讲,这个请求是完全合法的,但是却完成了攻击者所期望的一个操作。且你自己还不知道究竟是哪些操作。
包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账。
造成个人隐私泄露以及财产安全等问题。

预防

  1. 尽量使用 POST 接口,限制 GET 接口
    GET接口只要构造一个链接,便可以进行 CSRF 攻击。
    相比POST接口可降低攻击风险但不是万无一失( 攻击者只要构造一个 form 也可造成攻击 )。

  2. 加入验证码
    在提交的时候让用户去输入一下验证码,确保这个行为是一个用户行为而非黑客行为。

  3. 验证referer
    http协议 的头部有一个字段叫 referer,它能够记录当前一次请求的来源地址。如果黑客要对当前网站实施 CSRF 的攻击。他只能在自己的网站构造请求,所以 referer 传过来的是和当前网站不同的域名。我们可以在后端判断一下,如果 referer 值不是当前的网站,那么直接就拒绝这个请求

  4. 使用token
    在用户登录成功后,返回一个随机 token 给浏览器,当每次用户发送请求的时候,将 token 主动发送给服务器端(为了安全,不建议将 token 以参数的形式传给服务器,可以将token存储在请求头中),服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

# 追问:说说缓存 SessionStorage,LocalStorage,Cookie

sessionStorage 是会话级别存储,只要会话结束关闭窗口,sessionStorage 立即被销毁。
localStorage 是持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
sessionStroagelocalStroage 存储大小可以达到 5M,不能和服务器做交互。
cookie 的数据会始终在同源 http 请求中携带,在浏览器和服务器之间来回传递。单个cookie 不能超过4K,只在设置的 cookie 过期时间之前有效,即使窗口关闭或浏览器关闭 。很多浏览器都限制一个站点最多保存20个 Cookie

# 说说深拷贝 和 浅拷贝

浅拷贝:只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。如果其中一个对象改变了这个地址,就会影响到另一个对象。

  1. 直接用=赋值
  2. Object.assign
    只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是仍是对象的话依然是浅拷贝。
    Object.assign 还有一些注意的点是:
    (1)不会拷贝对象继承的属性
    (2)不可枚举的属性
    (3)属性的数据属性/访问器属性
    (4)可以拷贝Symbol类型
  3. for in 循环只遍历第一层

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

  1. JSON.stringify 把对象转换成字符串,再用 JSON.parse 把字符串转换成新的对象
    注意:属性值为函数时该属性会丢失,为正则时会转为空对象,为new Date()时会转为字符串
  2. 采用递归去拷贝所有层级属性
  3. Slice 实现对数组的深拷贝
  4. 使用扩展运算符实现深拷贝
// 递归算法实现深克隆
function deepClone(obj){
  if(obj === null) return null;
  if(typeof obj !=='object') return obj;
  if(obj instanceof RegExp) return new RegExp(obj);
  if(obj instanceof Date) return new Date(obj);

  // 克隆的结果和之前保持相同的所属类
  let newObj = new obj.constructor;
  for(let key in obj){
    if(obj.hasOwnProperty(key)){
        newObj[key] = deepFn(obj[key]);
    }
  }
  return newObj
}

# .说说 DOM 和 BOM

JavaScript 的实现包括以下3个部分:

  1. ECMAScript (核心) : 描述了 JS 的语法 和 基本对象。
  2. 文档对象模型 (DOM): 处理 网页内容 的方法和接口。
    W3C 的标准( 所有浏览器公共遵守的标准 )
  3. 浏览器对象模型 (BOM): 与 浏览器交互 的方法和接口。
    各个浏览器厂商根据 DOM 在各自浏览器上的实现( 不同厂商之间实现存在差异 )

DOM 的 API :

  1. 节点创建型 API:
    document.createElement(),document.createTextNode(),parent.cloneNode(true)
    document.createDocumentFragment() 创建文档片段,解决大量添加节点造成的回流问题

  2. 页面修改型 API:
    parent.appendChild(child),parent.removeChild(child)
    parent.replcaeChild(newChild,oldChild)
    parent.insertBefore(newNode, referenceNode)

  3. 节点查询型 API:
    document.getElementById()
    document.getElementsByTagName() 返回即时的 HTMLCollection 类型
    document.getElementsByName() 根据指定的 name 属性获取元素,返回即时的 NodeList
    document.getElementsByClassName() 返回即时的 HTMLCollection
    document.querySelector() 获取匹配到的第一个元素,采用的是深度优先搜索
    docuemnt.querySelectorAll() 返回非即时的 NodeList,也就是说结果不会随着文档树的变化而变化

  4. 节点关系型 API:
    父关系型:
    node.parentNode()
    兄弟关系型:
    node.previouSibling() 返回节点的前一个节点(包括元素节点,文本节点,注释节点)
    node.previousElementSibling() 返回前一个元素节点
    node.nextSibling() 返回下一个节点
    node.nextElementSibling() 返回下一个元素节点
    子关系型
    parent.childNodes() 返回一个即时的NodeList,包括了文本节点和注释节点
    parent.children() 一个即时的HTMLCollection,子节点都是Element
    parent.firsrtNode(),parent.lastNode(),hasChildNodes()

  5. 元素属性型 API:
    element.setAttribute(“name”,“value”) 为元素添加属性
    element.getAtrribute(“name”) 获取元素的属性

  6. 元素样式型 API:
    window.getComputedStyle(element) 返回一个CSSStyleDeclaration,可以从中访问元素的任意样式属性。
    element.getBoundingClientRect() 返回一个DOMRect对象,里面** 包括了元素相对于可视区的位置 top,left**,以及元素的大小,单位为纯数字。可用于判断某元素是否出现在了可视区域。

BOM 的 API :

  1. location对象
    .href、.search、.hash、.port、.hostname、pathname
  2. history对象
    .go(n)(前进或后退指定的页面数)、history.back(后退一页)、.forward(前进一页)
  3. navigator对象
    navigator:包含了用户浏览器的信息
    navigator.userAgent:返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串)
    navigator.cookieEnabled:返回浏览器是否支持(启用) cookie

window对象方法:

  1. alert() -- 显示带有一段消息和一个确认按钮的警告弹出框。
  2. confirm() -- 显示带有一段消息以及确认按钮和取消按钮的警告弹出框。
  3. prompt() -- 显示带有一段消息以及可提示用户输入的对话框和确认,取消的警告弹出框。
  4. open() -- 打开一个新的浏览器窗口或查找一个已命名的窗口。
  5. close() -- 关闭浏览器窗口。
  6. setInterval() -- 按照指定的周期(以毫秒计)来调用函数或计算表达式。每隔多长时间执行一下这个函数
  7. clearInterval() -- 取消由 setInterval() 设置的 timeout。
  8. setTimeout() -- 在指定的毫秒数后调用函数或计算表达式。
  9. clearTimeout() -- 取消由 setTimeout() 方法设置的 timeout。
  10. scrollTo() -- 把内容滚动到指定的坐标。

# .垃圾回收机制

标记清除法(常用): (1).标记阶段:垃圾回收器会从根对象开始遍历。每一个可以从根对象访问到的对象都会被添加一个标识,并称为可到达对象; (2).清除阶段:对堆内存从头到尾进行线性遍历,如发现有对象没有被标识为可到达对象,则将此对象占用的内存回收,并且将原来标记为可到达对象的标识清除,以便进行下一次垃圾回收操作;

优点: 实现简单
缺点: 可能会造成大量的内存碎片

引用计数清除法: (1).引用计数的含义就是跟踪记录每个值被引用的次数,当声明了一个变量并将一个引用类型赋值给该变量时,这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,这个值的引用次数就减1。 (2).当这个引用次数变成0时,则说明没有办法再访问这个值了,就可以将其所占的内存空间给回收。这样,垃圾收集器下次再运行时,就会释放那些引用次数为0的值所占的内存。

优点: 可即刻回收垃圾
缺点: 计数器值的增减处理繁重,实现繁琐复杂,循环引用无法回收

存在的问题: 如何避免 GC 造成的长时间停止响应?

  • 原因:
    GC 时为了安全考虑会停止响应其他操作。而 Javascript 的 GC 在 100ms 甚至以上,对一般的应用还好,但对于JS 游戏、动画这些要求连贯性比较高的应用就麻烦了。
  • 优化策略:
    (1). 分代回收(Generation GC)
    通过区分 临时持久 对象;多回收 临时对象 区(young generation),少回收 持久对象区(tenured generation),减少每次需遍历的对象,从而减少每次GC的耗时。
    (2). 增量GC
    每次处理一点,下次再处理一点,如此类推

# .js 延迟加载的方式有哪些?

  1. defer
    会告诉浏览器立即下载,但延迟整个页面都解析完毕之后再执行
    按顺序依次执行
  2. async
    不让页面等待脚本下载和执行,从而异步加载页面其他内容。
    将会在下载后尽快执行,不能保证脚本会按顺序执行( 在onload 事件之前完成 )。
  3. 动态创建DOM方式(创建script,插入到DOM中,加载完毕后callBack)
  4. 使用 setTimeout 延迟方法
  5. 让 JS 最后加载

# .说说跨域

跨域:指一个域下的文档或脚本试图去请求另一个域下的资源,由于浏览器同源策略限制而产生。
同源策略: 同协议+同端口+同域名。即便两个不同的域名指向同一个ip地址,也非同源。
如果缺少了 同源策略,浏览器很容易受到 XSS、CSFR 等攻击。

解决方案:

  1. Vue 配置代理类 proxy
  2. jsonp 利用标签没有跨越的特点,单只能实现get请求不能post请求
  3. CORS 跨域资源共享,只服务端设置Access-Control-Allow-Origin即可,前端无须设置
  4. nginx代理转发
  5. window.name + iframe跨域: 通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域
  6. location.hash + iframe: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
  7. document.domain + iframe跨域(仅限主域相同,子域不同的跨域应用场景):两个页面都通过js强制设置document.domain为基础主域,就实现了同域;

# .原生 AJAX 请求步骤

  1. 创建 XMLHTTPRequest 对象
    var xhr = new XMLHttpRequest();
  2. 使用 open 方法和服务器的交互信息
    `xhr.open('GET', 'example.txt', true);
  3. 使用 send 发送数据,开始和服务器端交互
    xhr.send('msg');
  4. 接收响应
    xhr.onreadystatechange =function(){}
    (1). 当readystate值从一个值变为另一个值时,都会触发 readystatechange 事件。
    (2). 当 readystate==4 时,表示已经接收到全部响应数据。
    (3). 当 status ==200 时,表示服务器成功返回页面和数据。
    (4). 如果(2)和(3)内容同时满足,则可以通过 xhr.responseText 获得服务器返回的内容。

# .for in 和 for of 的区别

  • for in 遍历的是数组的索引,在 for in
    (1). for inindex 索引为字符串型数字,不能直接进行几何运算
    (2). for in 遍历顺序有可能不是按照实际数组的内部顺序
    (3). 因为 for in 是遍历可枚举的属性,也包括原型上的属性( 如不想遍历原型上的属性,可通过 hasOwnProperty 判断某个属性是属于原型 还是 实例上 )。

  • for of 遍历的是数组的元素值
    for of 只是遍历数组的内部,不会遍历原型上的属性和索引
    也可以通过ES5的 Object.keys(obj) 来获取实例对象上的属性组成的数组

一般是使用 for in 来遍历对象,for of 遍历数组

# .mouseover 和 mouseenter 的区别

  • mouseover:当鼠标移入元素或其子元素都会触发事件,所以会重复触发冒泡的过程。
    对应的移出事件是 mouseout
  • mouseenter:鼠标移入子元素时不会再次触发事件
    对应的移出事件是 mouseleave

# .instanceof 的原理是什么?

function myInstanceof(left, right) {
  let prototype = right.prototype
  left = left.__proto__
  while (true) {
    if (left === null || left === undefined)
      return false
    if (prototype === left)
      return true
    left = left.__proto__
  }
}
思路:
首先获取类型的原型
然后获得对象的原型
然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null

# .keyup、keydown 和 keypress三种键盘事件的区别

keyup:松开键盘触发
keydown:按下键盘触发
keypress:不能识别功能键,比如 ctrl、alt、shift 左右箭头,可以区分大小写。

在输入框中按下一个键的全过程:
触发keydown / keypress 事件 ==> 文字键入输入框中 => 触发 keyup 事件

按下按键后自动对焦输入框,应该使用 keyup,不应该使用 keydown / keypress,因为后者会使按键落入输入框中,对于回车键的话还不能使用 keypress

# .定时器实现动画的最佳时间

大多数电脑显示器的刷新频率是 60HZ,大概相当于每秒钟重绘 60 次。
因此,最平滑的动画效的最佳循环间隔是 1000ms/60,约等于16.6ms

# . setInterval 存在的问题

定时器的代码执行部分不断的被调入任务队列中,如果定时器的执行时间比间隔时间长,最终可能导致定时器堆叠在一起执行。

js 引擎为了解决这个问题,采用的方式是若任务队列中存在这个定期器,则不会将新的定时器放入任务队列,这样做的弊端是可能导致某些间隔被跳过。

解决方法:循环调用setTimeout来实现setInterval:(即用setTimeout来实现setInterval)

 setTimeout(function fn(){
    ...
    setTimeout(fn,delay)
},delay)

# .requestAnimationFrame

js 动画的要求:

  1. 循环间隔必须足够短,这样才能让不同的动画效果显得平滑流畅。
  2. 循环间隔还要足够长,这样才能确保浏览器有能力渲染产生的变化。

用定时器实现 js 动画存在的问题:
定时器回调函数执行的时机不精确。定时器中的延时指的是将回调函数加入到任务队列所需花的时间,如果主线程中还有任务在执行,就不能确保回调函数在放入队列后马上执行,这就造成了执行时机的不精确。

requestAnimationFrame 特点:
采用系统时间间隔,保证了最佳的绘制效率。

requestAnimationFrame 使用方法:
接收一个回调函数,这个回调函数会在下一次浏览器重绘之前调用。

# .实现一个 sleep 函数

由于 js 是单线程的,可以利用伪死循环阻塞主线程来达到延迟执行的效果

function sleep(delay) {
    // 获取一个初始时间
    let startTime = new Date().getTime()
    // 如果时间差小于延迟时间,就一直循环
    while (new Date().getTime() - startTime < delay) {
        continue
    }
}

.说一说重载

重载: 相同函数名,不同参数列表的多个函数,在调用时可自动根据传入参数的不同,选择对应的函数执行。
严格意义上讲 js 中是没有重载的,因为后定义的函数会覆盖前面的同名函数,但是我们可以通过一些方法来模拟重载。

第一种方法:

利用函数内部的 argument, 在内部用 switch 语句,根据传入参数的 个数 或 类型 调用不同的 case 语句,从而功能上达到重载的效果。

第二种方法:
运用闭包原理,既然 js 后面的函数会覆盖前面的同名函数,就强行让所有的函数都留在内存里,等需要的时候再去找它。

function methodFn(obj, name, func){
    var old = obj[name];
    obj[name] = function(){
        if(arguments.length === fnc.length){
            return fnc.apply(this, arguments);
         }else if(typeof old === "function"){
            return old.apply(this, arguments);
         }
     }
}
var people = { };

method(people, "find", function(){
  console.log("无参数 ~ code");
})

method(people,"find",function(firstname){
  console.log("一个参数 ~ code");
})
method(people,"find",function(firstname,lastname){
  console.log("两个参数 ~ code");
})
        
people.find();

# .什么是作用域链(scope chain)

作用域链: 由各级作用域对象连续引用,形成的链式结构

函数的声明周期:

  1. 程序开始执行前: 程序会创建全局作用域对象window
  2. 定义函数时
    在window中创建函数名变量引用函数对象
    函数对象的隐藏属性scope指回函数来自的全局作用域对象window
  3. 调用函数时
    创建本次函数调用时使用的AO对象
    在AO对象中添加函数的局部变量
    设置AO的隐藏属性parent 指向函数的祖籍作用域对象。——执行时,如果AO中没有的变量可延parnet向祖籍作用域对象找。
  4. 函数调用后
    函数作用域对象AO释放
    导致AO中局部变量释放

作用

  1. 保存所有的变量
  2. 控制变量的使用顺序: 先用局部,局部没有才延作用域链向下查找。

# 列举几条 JS 的基本代码规范

  1. 变量和函数命名要见名知意
  2. 当命名对象、函数和实例时使用驼峰命名规则
  3. 请使用 === / !== 来值的比较
  4. 对字符串使用单引号 ''(因为大多时候我们的字符串。特别html会出现")
  5. switch 语句必须带有 default 分支
  6. 语句结束一定要加分号
  7. for 循环必须使用大括号
  8. 使用 /*.../ 进行多行注释,包括描述,指定类型以及参数值和返回值

# .Virtual Dom 的优势在哪里?

重点: VDOM 想解决的问题以及为什么频繁的 DOM 操作会性能差?

首先我们需要知道:

DOM 引擎、JS 引擎 相互独立,但又工作在同一线程(主线程) JS 代码调用 DOM API 必须 挂起 JS 引擎、转换传入参数数据、激活 DOM 引擎,DOM 重绘后再转换可能有的返回值,最后激活 JS 引擎并继续执行若有频繁的 DOM API 调用,且浏览器厂商不做“批量处理”优化, 引擎间切换的单位代价将迅速积累若其中有强制重绘的 DOM API 调用,重新计算布局、重新绘制图像会引起更大的性能消耗。

其次是 VDOM 和真实 DOM 的区别和优化:

虚拟 DOM 不会立马进行排版与重绘操作
虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分,最后在真实 DOM 中进行排版与重绘,减少过多DOM节点排版与重绘损耗
虚拟 DOM 有效降低大面积真实 DOM 的重绘与排版,因为最终与真实 DOM 比较差异,可以只渲染局部。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容