# 说一下JS 中的数据类型有哪些
JS 数据类型包括 基本 / 引用 / 特殊 数据类型:
- 基本数据类型:
String
、Number
、Boolean
- 引用数据类型:
Object
、Array
、Function
- 特殊数据类型:
Null
、Undefined
- 原始数据类型
Symbol
(ES6)
独一无二的值,即Symbol('1’) != Symbol('1’)
# 追问:判断 JS 数据类型有几种方法
常用的有 typeof、instanceof
,
不常用的有 constructor、 prototype / toString
-
typeof
是个一元运算,放在任意类型的运算数之前,返回一个字符串
说明运算数的类型。
可检测出的类型有:
'number'
、'string'
、'boolean'
、'object'
'undefined'
,'function'
、'symbol'
其中对象"object"
包括:Object
、Array
、new RegExp()
、new Date()
和Null 特殊类型
缺点:判断普通类型没有问题,但不能准确判断引用数据类型
-
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
是一切对象的父对象
- 根据对象的
constructor
判断
原理:每个构造函数都有一个constructor
属,指回它本身
[].coconstructor === Array ==> true
判断 数字、字符串、函数 和 日期时,必须得用关键字
new
创建才行
因为只有构造函数才有constructor
属性,还有两点需要注意:
null
和undefined
是无效的对象,因此不会有constructor
存在,- 函数的
constructor
是不稳定的,当重写prototype
后,
原有的constructor
引用会丢失,constructor
会默认为Object
- 使用
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]'
- 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
。
也可以理解是表示程序级的、正常的或在意料之中的值的空缺
- 作为函数的参数,表示该函数的参数不是对象
- 作为对象原型链的终点
注意:
null
不是一个对象,但typeof null === object
原因是不同的对象在底层都会表示为二进制,在 JS 中如果二进制的前三位都为0
,就会被判断为object
类型,null
的二进制全为0
,自然前三位也是0
,所以typeof null === 'objcet'
。
undefined
:是 Undefined
类型,表示一个 无 的原始值
或 缺少值
,
即此处应该有一个值,但还没有定义,使用 typeof undefined === 'undefined'
,转为数值为 NaN
。
它是在 ECMAScript
第三版引入的预定义全局变量,为了区分 空指针对象
和 未初始化的变量
。
也可以理解是表示系统级的、出乎意料的或类似错误的值的空缺
- 变量被声但没有赋值时
- 调用函数时,应该提供的参数没有提供时
- 对象没有赋值的属性时,属性值为 undefined
- 函数没有返回值时,默认返回值为 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 关键字创建一个实例都做了什么?
- 像普通对象一样,形成自己的私有作用域( 形参赋值,变量提升 )
- 创建一个新对象,将
this
指向这个新对象( 构造函数的作用域赋给新对象 ) - 执行构造函数中的代码,为这个新对象添加属性、方法
- 返回这个新对象(
新对象
为构造函数的实例 )
手写一个 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. 构造函数继承
用 call
和 apply
将父类构造函数引入子类函数
即:在子类函数中做了父类函数的自执行
- 缺点:
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
:
call
和 apply
是临时的且立即执行,
bind
是永久绑定不立即执行,返回一个新函数,需要时再去执行这个新函数。
call:
call( thisObj, obj1, obj2... )
要求传入函数的参数必须单独传入
apply:
apply(t hisObj, [argArray] )
要求传入函数的参数必须放入数组中整体传入
apply会将数组打散为单个参数值分别传入
bind:
永久绑定函数中的 this
,作用如下:
- 创建一个和原函数功能完全一样的新函数.
- 将新函数中的
this
永久绑定为指定对象 - 将新函数中的部分固定参数提前永久绑定
# 说说 ES6、ES7、ES8 的新特性
ES6的特性:
- 类(class)
- 模块化(
Module
)导出(export
)导入(import
) - 箭头(Arrow)函数
- 函数参数默认值
- 模板字符串
- 延展操作符(Spread operator) 和 剩余运算符(rest operator)
- ES6中允许我们在设置一个对象的属性的时候不指定属性名
- Promise 异步编程的解决方案
- 支持
let
与const
块级作用域
ES7的特性
-
includes()
函数用来判断一个数组是否包含一个指定的值,返回true
/false
- 指数操作符在ES7中引入了指数运算符,具有与Math.pow(..)等效的计算结果
ES8的特性
- 加入了对 async/await 的支持,也就我们所说的异步函数
- Object.values() 是一个与 Object.keys() 类似的新函数,但返回的是 Object 自身属性的所有值,不包括继承的值
- Object.entries() 函数返回一个给定对象自身可枚举属性的键值对的数组
- String.padStart(targetLength,[padString]) 和 String.padEnd(targetLength,padString])
5.Object.getOwnPropertyDescriptors() 函数用来获取一个对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。
# require 和 import 区别
import
和 require
都是被模块化所使用。
- 遵循规范
require
是AMD
规范引入方式
import
是es6
的语法标准,如要兼容浏览器的话必须转化成es5
的语法 - 调用时间
require
是运行时调用,所以require
理论上可以运用在代码的任何地方
import
是编译时调用,所以必须放在文件开头 - 本质
require
是赋值过程,其实require
的结果就是对象、数字、字符串、函数等,再把require
的结果赋值给某个变量
import
是解构过程,但是目前所有的引擎都还没有实现import
,我们使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import
语法会被转码为require - 性能
require
的性能相对于import
稍低,因为require
是在运行时才引入模块并且还赋值给某个变量
import
只需要依据import
中的接口在编译时引入指定模块所以性能稍高
# 追问:Es6 Module 和 Common.js 的区别
CommonJS
- 对于基本数据类型,属于复制,会被模块缓存。可在另一个模块可以对该模块输出的变量重新赋值。
- 对于复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
- 当使用 require 命令加载某个模块时,就会运行整个模块的代码。
- common.js 同一个模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载就返回第一次运行的结果,除非手动清除系统缓存。
- 循环加载时,属于加载时执行。即脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被 "循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
ES6 Module 模块
- ES6 模块中的值属于动态只读引用。
只读
:不允许修改引入变量的值,import 的变量是只读的( 包括基本/复杂
数据类型 )。当模块遇到 import 命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
动态
:原始值发生变化,import 加载的值也会发生变化( 包括基本/复杂
数据类型)。 - 循环加载时,ES6 模块是动态引用( 只要两个模块之间存在某个引用,代码就能执行 )。
综上:
- common.js 是 module.exports / exports 导出,require 导入;ES6 则是 export 导出,import 导入。
- common.js 是运行时加载模块,ES6 是在静态编译期间就确定模块的依赖。
- ES6 在编译期间会将所有 import 提升到顶部,common.js 不会提升 require。
- common.js 导出的是一个值拷贝,会对加载结果进行缓存,一旦内部再修改这个值,则不会同步到外部。ES6 是导出的一个引用,内部修改可以同步到外部。
- 两者的循环导入的实现原理不同,common.js 是当模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。ES6 模块是动态引用,如果使用 import 从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用。
- common.js 中顶层的
this
指向这个模块本身,而 ES6 中顶层this
指向undefined
。
# 说说闭包
闭包
:有权限访问其它函数作用域内变量的函数。
在 JS
中,变量分为全局变量和局部变量,局部变量的作用域属于函数作用域,在函数执行完以后作用域就会被销毁,内存也会被回收。但是闭包是建立在函数内部的子函数,可访问上级作用域,所以上级函数执行完,作用域也不会被销毁。
闭包解决了什么?
- 闭包就是将 函数内部 和 外部 连接起来的桥梁。
由于闭包可以缓存上级作用域,这样函数外部就可以访问到函数内部的变量。 - 起到保护全局不受污染又能 隐藏变量 的作用。
应用:防抖 与 节流
函数去抖(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)
}
}
}
# 箭头函数与普通函数的区别
- 箭头函数比普通函数语法更加简洁。
- 箭头函数没有自己的
this
,它里面的this
是继承函数所处上下文的this
( 使用call/apply/bind
等任何方法都无法改变其指向)。 - 箭头函数没有
Arguments
(类数组),只能用 ...args 获取传递的参数结合( 数组 )。 - 箭头函数是匿名函数,不能使用
new
( 因为没有this
和prototype
属性 )。
# 事件委托是什么,原理是什么
事件委托
: 利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
原理
:利用事件的 冒泡原理
事件冒泡
:就是事件从最深的节点开始,然后逐步向上传播事件。
作用:
- 提高性能:每一个函数都会占用内存空间,只需添加一个事件处理程序代理所有事件,所占用的内存空间更少;
- 动态监听:使用事件委托可以自动绑定动态添加的元素,即新增的节点不需要主动添加也可以具有和其它元素一样的事件。
如何 阻止冒泡 和 默认事件
停止冒泡:
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单线程
的本质。
任务队列的本质
- 所有 同步任务 都在
主线程
上执行,形成一个执行栈(execution context stack)。 - 主线程之外,还有一个
任务队列
(task queue)。
只要 异步任务 有了运行结果,就在任务队列
之中放置一个事件。 - 等
执行栈
中的所有同步任务执行完毕,系统就会读取任务队列
,看看里面有哪些事件。
哪些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。 - 主线程不断重复上面的第三步。
主线程
(执行栈)和 任务队列
先进先出
的通信称为 事件循环
( 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 转换为字符串
# 追问:对象和数组有啥区别
对象
:是包含已命名的值的无序集合,也被称为 关联数组
数组
:是包含已编码的值的有序集合
- 创建方式不同,数组是
[] / new Array
,对象是{} / new Object
。 - 调用方式不同,数组是
arr[下标]
,对象是obj
.加属性名 / [属性名]
。 - 数组是有序数据的集合,对象是无序。
- 数组的数据没有名称,只有下标,对象的数据需要指定名称。
- 数组的元素可以重复,对象的属性是唯一的。
- 数组的有长度,而对象没有。
# 追问:数组常用的操作方法有哪些
操作数组: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 有何区别
相同点:
- 都是循环遍历数组中的每一项
- forEach 和 map方法里每次执行匿名函数都支持3个参数,
参数分别是item(当前每一项),index(索引值),arr(原数组) - 匿名函数中的
this
都是指向 window( 在 Vue 中指向 Vue 实例)
不同点:
- map() 返回一个新数组,原数组不会改变,可链式调用
- 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);
# 追问:什么是数组扁平化,实现扁平化的方法有哪些?
数组扁平化
:一个多维数组变为一维数组,方法如下:
- flat( ES 6)
flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
let newArray = arr.flat([depth]);
depth值可选: 指定要提取嵌套数组的结构深度,默认值为 1,不确定层级也可写 `Infinity`。
- reduce
function flatten(arr) {
return arr.reduce((result, item)=> {
return result.concat(Array.isArray(item) ? flatten(item) : item);
}, []);
}
- toString & split
function flatten(arr) {
return arr.toString().split(',').map(function(item) {
return Number(item);
})
}
- join & split
function flatten(arr) {
return arr.join(',').split(',').map(function(item) {
return parseInt(item);
})
}
- 扩展运算符
[].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;
}
- 递归
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)。
攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来讲,这个请求是完全合法的,但是却完成了攻击者所期望的一个操作。且你自己还不知道究竟是哪些操作。
包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账。
造成个人隐私泄露以及财产安全等问题。
预防
-
尽量使用 POST 接口,限制 GET 接口
GET接口只要构造一个链接,便可以进行 CSRF 攻击。
相比POST接口可降低攻击风险但不是万无一失( 攻击者只要构造一个 form 也可造成攻击 )。 -
加入验证码
在提交的时候让用户去输入一下验证码,确保这个行为是一个用户行为而非黑客行为。 -
验证referer
在http协议
的头部有一个字段叫referer
,它能够记录当前一次请求的来源地址。如果黑客要对当前网站实施 CSRF 的攻击。他只能在自己的网站构造请求,所以 referer 传过来的是和当前网站不同的域名。我们可以在后端判断一下,如果 referer 值不是当前的网站,那么直接就拒绝这个请求 -
使用token
在用户登录成功后,返回一个随机 token 给浏览器,当每次用户发送请求的时候,将 token 主动发送给服务器端(为了安全,不建议将 token 以参数的形式传给服务器,可以将token存储在请求头中),服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。
# 追问:说说缓存 SessionStorage,LocalStorage,Cookie
sessionStorage
是会话级别存储,只要会话结束关闭窗口,sessionStorage
立即被销毁。
localStorage
是持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
sessionStroage
和 localStroage
存储大小可以达到 5M,不能和服务器做交互。
cookie
的数据会始终在同源 http
请求中携带,在浏览器和服务器之间来回传递。单个cookie
不能超过4K,只在设置的 cookie
过期时间之前有效,即使窗口关闭或浏览器关闭 。很多浏览器都限制一个站点最多保存20个 Cookie
。
# 说说深拷贝 和 浅拷贝
浅拷贝
:只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。如果其中一个对象改变了这个地址,就会影响到另一个对象。
- 直接用=赋值
- Object.assign
只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是仍是对象的话依然是浅拷贝。
Object.assign 还有一些注意的点是:
(1)不会拷贝对象继承的属性
(2)不可枚举的属性
(3)属性的数据属性/访问器属性
(4)可以拷贝Symbol类型 - for in 循环只遍历第一层
深拷贝
:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
- 用
JSON.stringify
把对象转换成字符串,再用 JSON.parse 把字符串转换成新的对象
注意:属性值为函数
时该属性会丢失,为正则
时会转为空对象,为new Date()
时会转为字符串 - 采用递归去拷贝所有层级属性
- 用
Slice
实现对数组的深拷贝 - 使用扩展运算符实现深拷贝
// 递归算法实现深克隆
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个部分:
-
ECMAScript (核心)
: 描述了 JS 的语法 和 基本对象。 -
文档对象模型 (DOM)
: 处理 网页内容 的方法和接口。
W3C 的标准( 所有浏览器公共遵守的标准 ) -
浏览器对象模型 (BOM)
: 与 浏览器交互 的方法和接口。
各个浏览器厂商根据 DOM 在各自浏览器上的实现( 不同厂商之间实现存在差异 )
DOM
的 API :
-
节点创建型 API:
document.createElement(),document.createTextNode(),parent.cloneNode(true)
document.createDocumentFragment() 创建文档片段,解决大量添加节点造成的回流问题 -
页面修改型 API:
parent.appendChild(child),parent.removeChild(child)
parent.replcaeChild(newChild,oldChild)
parent.insertBefore(newNode, referenceNode) -
节点查询型 API:
document.getElementById()
document.getElementsByTagName() 返回即时的HTMLCollection
类型
document.getElementsByName() 根据指定的name
属性获取元素,返回即时的NodeList
document.getElementsByClassName() 返回即时的HTMLCollection
document.querySelector() 获取匹配到的第一个元素,采用的是深度优先搜索
docuemnt.querySelectorAll() 返回非即时的NodeList
,也就是说结果不会随着文档树的变化而变化 -
节点关系型 API:
父关系型:
node.parentNode()
兄弟关系型:
node.previouSibling() 返回节点的前一个节点(包括元素节点,文本节点,注释节点)
node.previousElementSibling() 返回前一个元素节点
node.nextSibling() 返回下一个节点
node.nextElementSibling() 返回下一个元素节点
子关系型
parent.childNodes() 返回一个即时的NodeList,包括了文本节点和注释节点
parent.children() 一个即时的HTMLCollection,子节点都是Element
parent.firsrtNode(),parent.lastNode(),hasChildNodes() -
元素属性型 API:
element.setAttribute(“name”,“value”) 为元素添加属性
element.getAtrribute(“name”) 获取元素的属性 -
元素样式型 API:
window.getComputedStyle(element) 返回一个CSSStyleDeclaration,可以从中访问元素的任意样式属性。
element.getBoundingClientRect() 返回一个DOMRect对象,里面** 包括了元素相对于可视区的位置 top,left**,以及元素的大小,单位为纯数字。可用于判断某元素是否出现在了可视区域。
BOM
的 API :
- location对象
.href、.search、.hash、.port、.hostname、pathname - history对象
.go(n)(前进或后退指定的页面数)、history.back(后退一页)、.forward(前进一页) - navigator对象
navigator:包含了用户浏览器的信息
navigator.userAgent:返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串)
navigator.cookieEnabled:返回浏览器是否支持(启用) cookie
window
对象方法:
- alert() -- 显示带有一段消息和一个确认按钮的警告弹出框。
- confirm() -- 显示带有一段消息以及确认按钮和取消按钮的警告弹出框。
- prompt() -- 显示带有一段消息以及可提示用户输入的对话框和确认,取消的警告弹出框。
- open() -- 打开一个新的浏览器窗口或查找一个已命名的窗口。
- close() -- 关闭浏览器窗口。
- setInterval() -- 按照指定的周期(以毫秒计)来调用函数或计算表达式。每隔多长时间执行一下这个函数
- clearInterval() -- 取消由 setInterval() 设置的 timeout。
- setTimeout() -- 在指定的毫秒数后调用函数或计算表达式。
- clearTimeout() -- 取消由 setTimeout() 方法设置的 timeout。
- 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 延迟加载的方式有哪些?
- defer
会告诉浏览器立即下载,但延迟整个页面都解析完毕之后再执行
按顺序依次执行 - async
不让页面等待脚本下载和执行,从而异步加载页面其他内容。
将会在下载后尽快执行,不能保证脚本会按顺序执行( 在onload 事件之前完成 )。 - 动态创建DOM方式(创建script,插入到DOM中,加载完毕后callBack)
- 使用 setTimeout 延迟方法
- 让 JS 最后加载
# .说说跨域
跨域:指一个域下的文档或脚本试图去请求另一个域下的资源,由于浏览器同源策略限制而产生。
同源策略:
同协议+同端口+同域名。即便两个不同的域名指向同一个ip地址,也非同源。
如果缺少了 同源策略
,浏览器很容易受到 XSS、CSFR
等攻击。
解决方案:
- Vue 配置代理类 proxy
- jsonp 利用标签没有跨越的特点,单只能实现get请求不能post请求
- CORS 跨域资源共享,只服务端设置Access-Control-Allow-Origin即可,前端无须设置
- nginx代理转发
- window.name + iframe跨域: 通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域
- location.hash + iframe: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
- document.domain + iframe跨域(仅限主域相同,子域不同的跨域应用场景):两个页面都通过js强制设置document.domain为基础主域,就实现了同域;
# .原生 AJAX 请求步骤
- 创建
XMLHTTPRequest
对象
var xhr = new XMLHttpRequest(); - 使用
open
方法和服务器的交互信息
`xhr.open('GET', 'example.txt', true); - 使用
send
发送数据,开始和服务器端交互
xhr.send('msg'); - 接收响应
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 in
中index
索引为字符串型数字,不能直接进行几何运算
(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 动画的要求:
- 循环间隔必须足够短,这样才能让不同的动画效果显得平滑流畅。
- 循环间隔还要足够长,这样才能确保浏览器有能力渲染产生的变化。
用定时器实现 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)
作用域链: 由各级作用域对象连续引用,形成的链式结构
函数的声明周期:
- 程序开始执行前: 程序会创建全局作用域对象window
- 定义函数时
在window中创建函数名变量引用函数对象
函数对象的隐藏属性scope指回函数来自的全局作用域对象window - 调用函数时
创建本次函数调用时使用的AO对象
在AO对象中添加函数的局部变量
设置AO的隐藏属性parent 指向函数的祖籍作用域对象。——执行时,如果AO中没有的变量可延parnet向祖籍作用域对象找。 - 函数调用后
函数作用域对象AO释放
导致AO中局部变量释放
作用
- 保存所有的变量
- 控制变量的使用顺序: 先用局部,局部没有才延作用域链向下查找。
# 列举几条 JS 的基本代码规范
- 变量和函数命名要见名知意
- 当命名对象、函数和实例时使用驼峰命名规则
- 请使用 === / !== 来值的比较
- 对字符串使用单引号 ''(因为大多时候我们的字符串。特别html会出现")
- switch 语句必须带有 default 分支
- 语句结束一定要加分号
- for 循环必须使用大括号
- 使用 /*.../ 进行多行注释,包括描述,指定类型以及参数值和返回值
# .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 比较差异,可以只渲染局部。
网友评论