最近在整理 JavaScript 的时候发现遇到了不少面试中常见的面试题,本部分主要是做者在 Github 等各大论坛收录的 JavaScript 相关知识和一些相关面试题时所作的笔记,分享这份总结给你们,对你们对 JavaScript 的能够来一次全方位的检漏和排查。文章末尾有彩蛋不要错过!javascript
js 一共有六种基本数据类型,分别是 Undefined、Null、Boolean、Number、String,还有在 ES6 中新增的 Symbol 类型,表明建立后独一无二且不可变的数据类型,它的出现我认为主要是为了解决可能出现的全局变量冲突的问题。html
js 能够分为两种类型的值,一种是基本数据类型,一种是复杂数据类型。前端
基本数据类型....(参考1)vue
复杂数据类型指的是 Object 类型,全部其余的如 Array、Date 等数据类型均可以理解为 Object 类型的子类。java
两种类型间的主要区别是它们的存储位置不一样,基本数据类型的值直接保存在栈中,而复杂数据类型的值保存在堆中,经过使用在栈中保存对应的指针来获取堆中的值。node
堆和栈的概念存在于数据结构中和操做系统内存中。程序员
在数据结构中,栈中数据的存取方式为先进后出。而堆是一个优先队列,是按优先级来进行排序的,优先级能够按照大小来规定。彻底二叉树是堆的一种实现方式。es6
在操做系统中,内存被分为栈区和堆区。web
栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操做方式相似于数据结构中的栈。面试
堆区内存通常由程序员分配释放,若程序员不释放,程序结束时可能由垃圾回收机制回收。
全部 typeof 返回值为 "object" 的对象(如数组)都包含一个内部属性 [[Class]](咱们能够把它看做一个内部的分类,而非 传统的面向对象意义上的类)。这个属性没法直接访问,通常经过 Object.prototype.toString(..) 来查看。例如: Object.prototype.toString.call( [1,2,3] ); // "[object Array]" Object.prototype.toString.call( /regex-literal/i ); // "[object RegExp]"
js 中的内置对象主要指的是在程序执行前存在全局做用域里的由 js 定义的一些全局值属性、函数和用来实例化其余对象的构造函数对象。
通常咱们常常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。
已在做用域中声明但尚未赋值的变量,是 undefined 的。相反,尚未在做用域中声明过的变量,是 undeclared 的。
对于 undeclared 变量的引用,浏览器会报引用错误,如 ReferenceError: b is not defined 。可是咱们可使用 typeof 的安全防范机制来避免报错,由于对于 undeclared(或者 not defined )变量,typeof 会返回 "undefined"。
首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
undefined 表明的含义是未定义,null 表明的含义是空对象。通常变量声明了但尚未定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,做为初始化。
undefined 在 js 中不是一个保留字,这意味着咱们可使用 undefined 来做为一个变量名,这样的作法是很是危险的,它会影响咱们对 undefined 值的判断。可是咱们能够经过一些方法得到安全的 undefined 值,好比说 void 0。
当咱们对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 “object”,这是一个历史遗留的问题。当咱们使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
由于 undefined 是一个标识符,因此能够被看成变量来使用和赋值,可是这样会影响 undefined 的正常判断。
表达式 void ___ 没有返回值,所以返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值。
按惯例咱们用 void 0 来得到 undefined。
在日常项目开发中,咱们遵照一些这样的基本规范,好比说:
(1)一个函数做用域中全部的变量声明应该尽可能提到函数首部,用一个 var 声明,不容许出现两个连续的 var 声明,声明时若是变量没有值,应该给该变量赋值对应类型的初始值,便于他人阅读代码时,可以一目了然的知道变量对应的类型值。
(2)代码中出现地址、时间等字符串时须要使用常量代替。
(3)在进行比较的时候吧,尽可能使用'=', '!'代替'==', '!='。
(4)不要在内置对象的原型上添加方法,如 Array, Date。
(5)switch 语句必须带有 default 分支。
(6)for 循环必须使用大括号。
(7)if 语句必须使用大括号。
在 js 中咱们是使用构造函数来新建一个对象的,每个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对象,这个对象包含了能够由该构造函数的全部实例共享的属性和方法。当咱们使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。
通常来讲咱们是不该该可以获取到这个值的,可是如今浏览器中都实现了__proto__ 属性来让咱们访问这个属性,可是咱们最好不要使用这个属性,由于它不是规范中规定的。ES5 中新增了一个Object.getPrototypeOf() 方法,咱们能够经过这个方法来获取对象的原型。
当咱们访问一个对象的属性时,若是这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有本身的原型,因而就这样一直找下去,也就是原型链的概念。原型链的尽头通常来讲都是 Object.prototype 因此这就是咱们新建的对象为何可以使用 toString() 等方法的缘由。
特色:
JavaScript 对象是经过引用来传递的,咱们建立的每一个新对象实体中并无一份属于本身的原型副本。当咱们修改原型时,与之相关的对象也会继承这一改变。
安全整数指的是,在这个范围内的整数转化为二进制存储的时候不会出现精度丢失,可以被“安全”呈现的最大整数是 2^53 - 1,即9007199254740991,在 ES6 中被定义为 Number.MAX_SAFE_INTEGER。最小整数是-9007199254740991,在 ES6 中被定义Number.MIN_SAFE_INTEGER。
若是某次计算的结果获得了一个超过 JavaScript 数值范围的值,那么这个值会被自动转换为特殊的 Infinity 值。若是某次计算返回了正或负的 Infinity 值,那么该值将没法参与下一次的计算。判断一个数是否是有穷的,可使用 isFinite 函数来判断。
NaN 意指“不是一个数字”(not a number),NaN 是一个“警惕值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误状况,即“执行数学运算没有成功,这是失败后返回的结果”。
typeof NaN; // "number"
NaN 是一个特殊值,它和自身不相等,是惟一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN != NaN为 true。
函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,所以非数字值传入也会返回 true ,会影响 NaN 的判断。
函数 Number.isNaN 会首先判断传入参数是否为数字,若是是数字再继续判断是否为 NaN ,这种方法对于 NaN 的判断更为准确。
Array 构造函数只带一个数字参数的时候,该参数会被做为数组的预设长度(length),而非只充当数组中的一个元素。这样建立出来的只是一个空数组,只不过它的 length 属性被设置成了指定的值。
构造函数 Array(..) 不要求必须带 new 关键字。不带时,它会被自动补上。
规范的 9.8 节中定义了抽象操做 ToString ,它负责处理非字符串到字符串的强制类型转换。
(1)Null 和 Undefined 类型 ,null 转换为 "null",undefined 转换为 "undefined",
(2)Boolean 类型,true 转换为 "true",false 转换为 "false"。
(3)Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
(4)Symbol 类型的值直接转换,可是只容许显式强制类型转换,使用隐式强制类型转换会产生错误。
(3)对普通对象来讲,除非自行定义 toString() 方法,不然会调用toString((Object.prototype.toString())来返回内部属性 [[Class]] 的值,如"[object Object]"。若是对象有本身的 toString() 方法,字符串化时就会调用该方法并使用其返回值。
有时咱们须要将非数字值看成数字来使用,好比数学运算。为此 ES5 规范在 9.3 节定义了抽象操做 ToNumber。
(1)Undefined 类型的值转换为 NaN。
(2)Null 类型的值转换为 0。
(3)Boolean 类型的值,true 转换为 1,false 转换为 0。
(4)String 类型的值转换如同使用 Number() 函数进行转换,若是包含非数字值则转换为 NaN,空字符串为 0。
(5)Symbol 类型的值不能转换为数字,会报错。
(6)对象(包括数组)会首先被转换为相应的基本类型值,若是返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
为了将值转换为相应的基本类型值,抽象操做 ToPrimitive 会首先(经过内部操做 DefaultValue)检查该值是否有valueOf() 方法。若是有而且返回基本类型值,就使用该值进行强制类型转换。若是没有就使用 toString() 的返回值(若是存在)来进行强制类型转换。
若是 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
ES5 规范 9.2 节中定义了抽象操做ToBoolean,列举了布尔强制类型转换全部可能出现的结果。
如下这些是假值:
• undefined
• null
• false
• +0、-0 和 NaN
• ""
假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表之外的都应该是真值。
{} 的 valueOf 结果为 {} ,toString 的结果为 "[object Object]"
[] 的 valueOf 结果为 [] ,toString 的结果为 ""
浏览器在某些特定状况下,在常规 JavaScript 语法基础上本身建立了一些外来值,这些就是“假值对象”。假值对象看起来和普通对象并没有二致(都有属性,等等),但将它们强制类型转换为布尔值时结果为 false 最多见的例子是 document.all,它是一个类数组对象,包含了页面上的全部元素,由 DOM(而不是 JavaScript 引擎)提供给 JavaScript 程序使用。
~ 返回 2 的补码,而且 ~ 会将数字转换为 32 位整数,所以咱们可使用 ~ 来进行取整操做。
~x 大体等同于 -(x+1)。
解析容许字符串(如 parseInt() )中含有非数字字符,解析按从左到右的顺序,若是遇到非数字字符就中止。而转换(如 Number ())不容许出现非数字字符,不然会失败并返回 NaN。
据 ES5 规范 11.6.1 节,若是某个操做数是字符串或者可以经过如下步骤转换为字符串的话,+ 将进行拼接操做。若是其中一个操做数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操做,该抽象操做再调用[[DefaultValue]],以数字做为上下文。若是不能转换为字符串,则会将其转换为数字类型来进行计算。
简单来讲就是,若是 + 的其中一个操做数是字符串(或者经过以上步骤最终获得字符串),则执行字符串拼接,不然执行数字加法。
那么对于除了加法的运算符来讲,只要其中一方是数字,那么另外一方就会被转为数字。
(1) if (..) 语句中的条件判断表达式。
(2) for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。
(3) while (..) 和 do..while(..) 循环中的条件判断表达式。
(4) ? : 中的条件判断表达式。
(5) 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操做数(做为条件判断表达式)。
|| 和 && 首先会对第一个操做数执行条件判断,若是其不是布尔值就先进行 ToBoolean 强制类型转换,而后再执行条件
判断。
对于 || 来讲,若是条件判断结果为 true 就返回第一个操做数的值,若是为 false 就返回第二个操做数的值。
&& 则相反,若是条件判断结果为 true 就返回第二个操做数的值,若是为 false 就返回第一个操做数的值。
|| 和 && 返回它们其中一个操做数的值,而非条件判断的结果
ES6 容许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误。
Symbol 值不可以被强制类型转换为数字(显式和隐式都会产生错误),但能够被强制类型转换为布尔值(显式和隐式结果都是 true )。
(1)字符串和数字之间的相等比较,将字符串转换为数字以后再进行比较。
(2)其余类型和布尔类型之间的相等比较,先将布尔值转换为数字后,再应用其余规则进行比较。
(3)null 和 undefined 之间的相等比较,结果为真。其余值和它们进行比较都返回假值。
(4)对象和非对象之间的相等比较,对象先调用 ToPrimitive 抽象操做后,再进行比较。
(5)若是一个操做值为 NaN ,则相等比较返回 false( NaN 自己也不等于 NaN )。
(6)若是两个操做值都是对象,则比较它们是否是指向同一个对象。若是两个操做数都指向同一个对象,则相等操做符返回 true,不然,返回 false。
(1)使用 Number() 方法,前提是所包含的字符串不包含不合法字符。
(2)使用 parseInt() 方法,parseInt() 函数可解析一个字符串,并返回一个整数。还能够设置要解析的数字的基数。当基数的值为 0,或没有设置该参数时,parseInt() 会根据 string 来判断数字的基数。
(3)使用 parseFloat() 方法,该函数解析一个字符串参数并返回一个浮点数。
(4)使用 + 操做符的隐式转换。
function format(number) { return number && number.replace(/(?!^)(?=(\d{3})+\.)/g, ","); }
// (1)匹配 16 进制颜色值 var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g; // (2)匹配日期,如 yyyy-mm-dd 格式 var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/; // (3)匹配 qq 号 var regex = /^[1-9][0-9]{4,10}$/g; // (4)手机号码正则 var regex = /^1[34578]\d{9}$/g; // (5)用户名正则 var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
// (1)使用数组 sort 方法对数组元素随机排序,让 Math.random() 出来的数与 0.5 比较,若是大于就返回 1 交换位置,若是小于就返回 -1,不交换位置。 function randomSort(a, b) { return Math.random() > 0.5 ? -1 : 1; } // 缺点:每一个元素被派到新数组的位置不是随机的,缘由是 sort() 方法是依次比较的。 // (2)随机从原数组抽取一个元素,加入到新数组 function randomSort(arr) { var result = []; while (arr.length > 0) { var randomIndex = Math.floor(Math.random() * arr.length); result.push(arr[randomIndex]); arr.splice(randomIndex, 1); } return result; } // (3)随机交换数组内的元素(洗牌算法相似) function randomSort(arr) { var index, randomIndex, temp, len = arr.length; for (index = 0; index < len; index++) { randomIndex = Math.floor(Math.random() * (len - index)) + index; temp = arr[index]; arr[index] = arr[randomIndex]; arr[randomIndex] = temp; } return arr; } // es6 function randomSort(array) { let length = array.length; if (!Array.isArray(array) || length <= 1) return; for (let index = 0; index < length - 1; index++) { let randomIndex = Math.floor(Math.random() * (length - index)) + index; [array[index], array[randomIndex]] = [array[randomIndex], array[index]]; } return array; }
咱们通常使用字面量的形式直接建立对象,可是这种建立方式对于建立大量类似对象的时候,会产生大量的重复代码。但 js和通常的面向对象的语言不一样,在 ES6 以前它没有类的概念。可是咱们可使用函数来进行模拟,从而产生出可复用的对象建立方式,我了解到的方式有这么几种:
(1)第一种是工厂模式,工厂模式的主要工做原理是用函数来封装建立对象的细节,从而经过调用函数来达到复用的目的。可是它有一个很大的问题就是建立出来的对象没法和某个类型联系起来,它只是简单的封装了复用代码,而没有创建起对象和类型间的关系。
(2)第二种是构造函数模式。js 中每个函数均可以做为构造函数,只要一个函数是经过 new 来调用的,那么咱们就能够把它称为构造函数。执行构造函数首先会建立一个对象,而后将对象的原型指向构造函数的 prototype 属性,而后将执行上下文中的 this 指向这个对象,最后再执行整个函数,若是返回值不是对象,则返回新建的对象。由于 this 的值指向了新建的对象,所以咱们可使用 this 给对象赋值。构造函数模式相对于工厂模式的优势是,所建立的对象和构造函数创建起了联系,所以咱们能够经过原型来识别对象的类型。
可是构造函数存在一个缺点就是,形成了没必要要的函数对象的建立,由于在 js 中函数也是一个对象,所以若是对象属性中若是包含函数的话,那么每次咱们都会新建一个函数对象,浪费了没必要要的内存空间,由于函数是全部的实例均可以通用的。
(3)第三种模式是原型模式,由于每个函数都有一个 prototype 属性,这个属性是一个对象,它包含了经过构造函数建立的全部实例都能共享的属性和方法。所以咱们可使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来讲,解决了函数对象的复用问题。可是这种模式也存在一些问题,一个是没有办法经过传入参数来初始化值,另外一个是若是存在一个引用类型如 Array 这样的值,那么全部的实例将共享一个对象,一个实例对引用类型值的改变会影响全部的实例。
(4)第四种模式是组合使用构造函数模式和原型模式,这是建立自定义类型的最多见方式。由于构造函数模式和原型模式分开使用都存在一些问题,所以咱们能够组合使用这两种模式,经过构造函数来初始化对象的属性,经过原型对象来实现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,可是有一点不足的就是,由于使用了两种不一样的模式,因此对于代码的封装性不够好。
(5)第五种模式是动态原型模式,这一种模式将原型方法赋值的建立过程移动到了构造函数的内部,经过对属性是否存在的判断,能够实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。
(6)第六种模式是寄生构造函数模式,这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式同样,没法实现对象的识别。
我了解的 js 中实现继承的几种方式有:
(1)第一种是以原型链的方式来实现继承,可是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被全部的实例对象所共享,容易形成修改的混乱。还有就是在建立子类型的时候不能向超类型传递参数。
(2)第二种方式是使用借用构造函数的方式,这种方式是经过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,可是它存在的一个问题就是没法实现函数方法的复用,而且超类型原型定义的方法子类型也没有办法访问到。
(3)第三种方式是组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。经过借用构造函数的方式来实现类型的属性的继承,经过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,可是因为咱们是以超类型的实例来做为子类型的原型,因此调用了两次超类的构造函数,形成了子类型的原型中多了不少没必要要的属性。
(4)第四种方式是原型式继承,原型式继承的主要思路就是基于已有的对象来建立新的对象,实现的原理是,向函数中传入一个对象,而后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。
(5)第五种方式是寄生式继承,寄生式继承的思路是建立一个用于封装继承过程的函数,经过传入一个对象,而后复制一个对象的副本,而后对象进行扩展,最后返回这个对象。这个扩展的过程就能够理解是一种继承。这种继承的优势就是对一个简单对象实现继承,若是这个对象不是咱们的自定义类型时。缺点是没有办法实现函数的复用。
(6)第六种方式是寄生式组合继承,组合继承的缺点就是使用超类型的实例作为子类型的原型,致使添加了没必要要的原型属性。寄生式组合继承的方式是使用超类型的原型的副原本做为子类型的原型,这样就避免了建立没必要要的属性。
function Person(name) { this.name = name; } Person.prototype.sayName = function() { console.log("My name is " + this.name + "."); }; function Student(name, grade) { Person.call(this, name); this.grade = grade; } Student.prototype = Object.create(Person.prototype); Student.prototype.constructor = Student; Student.prototype.sayMyGrade = function() { console.log("My grade is " + this.grade + "."); };
做用域链的做用是保证对执行环境有权访问的全部变量和函数的有序访问,经过做用域链,咱们能够访问到外层环境的变量和函数。
做用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中全部变量和函数的对象。做用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是做用域链的最后一个对象。
当咱们查找一个变量时,若是当前执行环境中没有找到,咱们能够沿着做用域链向后查找。
做用域链的建立过程跟执行上下文的创建有关....
1.第一种是函数调用模式,当一个函数不是一个对象的属性时,直接做为函数来调用时,this 指向全局对象。
2.第二种是方法调用模式,若是一个函数做为一个对象的方法来调用时,this 指向这个对象。
3.第三种是构造器调用模式,若是一个函数用 new 调用时,函数执行前会新建立一个对象,this 指向这个新建立的对象。
4.第四种是 apply 、 call 和 bind 调用模式,这三个方法均可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其他参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法经过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其余状况下都不会改变。
它的功能是把对应的字符串解析成 JS 代码并运行。
应该避免使用 eval,不安全,很是耗性能(2次,一次解析成 js 语句,一次执行)。
DOM 指的是文档对象模型,它指的是把文档当作一个对象来对待,这个对象主要定义了处理网页内容的方法和接口。
BOM 指的是浏览器对象模型,它指的是把浏览器当作一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。BOM的核心是window,而 window 对象具备双重角色,它既是经过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都做为全局对象的一个属性或者方法存在。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,而且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。
const EventUtils = { // 视能力分别使用dom0||dom2||IE方式 来绑定事件 // 添加事件 addEvent: function(element, type, handler) { if (element.addEventListener) { element.addEventListener(type, handler, false); } else if (element.attachEvent) { element.attachEvent("on" + type, handler); } else { element["on" + type] = handler; } }, // 移除事件 removeEvent: function(element, type, handler) { if (element.removeEventListener) { element.removeEventListener(type, handler, false); } else if (element.detachEvent) { element.detachEvent("on" + type, handler); } else { element["on" + type] = null; } }, // 获取事件目标 getTarget: function(event) { return event.target || event.srcElement; }, // 获取 event 对象的引用,取到事件的全部信息,确保随时能使用 event getEvent: function(event) { return event || window.event; }, // 阻止事件(主要是事件冒泡,由于 IE 不支持事件捕获) stopPropagation: function(event) { if (event.stopPropagation) { event.stopPropagation(); } else { event.cancelBubble = true; } }, // 取消事件的默认行为 preventDefault: function(event) { if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } } };
1.事件是用户操做网页时发生的交互动做,好比 click/move, 事件除了用户触发的动做外,还能够是文档加载,窗口滚动和大小调整。事件被封装成一个 event 对象,包含了该事件发生时的全部相关信息( event 的属性)以及能够对事件进行的操做( event 的方法)。
2.事件处理机制:IE 支持事件冒泡、Firefox 同时支持两种事件模型,也就是:事件冒泡和事件捕获。
3.event.stopPropagation() 或者 ie 下的方法 event.cancelBubble = true;
事件是用户操做网页时发生的交互动做或者网页自己的一些操做,现代浏览器一共有三种事件模型。
第一种事件模型是最先的 DOM0 级模型,这种模型不会传播,因此没有事件流的概念,可是如今有的浏览器支持以冒泡的方式实现,它能够在网页中直接定义监听函数,也能够经过 js 属性来指定监听函数。这种方式是全部浏览器都兼容的。
第二种事件模型是 IE 事件模型,在该事件模型中,一次事件共有两个过程,事件处理阶段,和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。而后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查通过的节点是否绑定了事件监听函数,若是有则执行。这种模型经过 attachEvent 来添加监听函数,能够添加多个监听函数,会按顺序依次执行。
第三种是 DOM2 级事件模型,在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查通过的节点是否绑定了事件监听函数,若是有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数能够指定事件是否在捕获阶段执行。
事件委托本质上是利用了浏览器事件冒泡的机制。由于事件在冒泡过程当中会上传到父节点,而且父节点能够经过事件对象获取到目标节点,所以能够把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理。
使用事件代理咱们能够没必要要为每个子元素都绑定一个监听事件,这样减小了内存上的消耗。而且使用事件代理咱们还能够实现事件的动态绑定,好比说新增了一个子节点,咱们并不须要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。
parseInt() 函数能解析一个字符串,并返回一个整数,须要两个参数 (val, radix),其中 radix 表示要解析的数字的基数。(该值介于 2 ~ 36 之间,而且字符串中的数字不能大于 radix 才能正确返回数字结果值)。
此处 map 传了 3 个参数 (element, index, array),默认第三个参数被忽略掉,所以三次传入的参数分别为 "1-0", "2-1", "3-2"
由于字符串的值不能大于基数,所以后面两次调用均失败,返回 NaN ,第一次基数为 0 ,按十进制解析返回 1。
闭包是指有权访问另外一个函数做用域中变量的函数,建立闭包的最多见的方式就是在一个函数内建立另外一个函数,建立的函数能够访问到当前函数的局部变量。
闭包有两个经常使用的用途。
闭包的第一个用途是使咱们在函数外部可以访问到函数内部的变量。经过使用闭包,咱们能够经过在外部调用闭包函数,从而在外部访问到函数内部的变量,可使用这种方法来建立私有变量。
函数的另外一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,由于闭包函数保留了这个变量对象的引用,因此这个变量对象不会被回收。
其实闭包的本质就是做用域链的一个特殊的应用,只要了解了做用域链的建立过程,就可以理解闭包的实现原理。
有一些对属性的操做,使用这种方法没法拦截,好比说经过下标方式修改数组数据或者给对象新增属性,vue 内部经过重写函数解决了这个问题。在 Vue3.0 中已经不使用这种方式了,而是经过使用 Proxy 对对象进行代理,从而实现数据劫持。使用 Proxy 的好处是它能够完美的监听到任何方式的数据改变,惟一的缺点是兼容性的问题,由于这是 ES6 的语法。
use strict 是一种 ECMAscript5 添加的(严格)运行模式,这种模式使得 Javascript 在更严格的条件下运行。
设立"严格模式"的目的,主要有如下几个:
区别:
回答
use strict 指的是严格运行模式,在这种模式对 js 的使用添加了一些限制。好比说禁止 this 指向全局对象,还有禁止使用 with 语句等。设立严格模式的目的,主要是为了消除代码使用中的一些不安全的使用方式,也是为了消除 js 语法自己的一些不合理的地方,以此来减小一些运行时的怪异的行为。同时使用严格运行模式也可以提升编译的效率,从而提升代码的运行速度。
我认为严格模式表明了 js 一种更合理、更安全、更严谨的发展方向。
第一种方式是使用 instanceof 运算符来判断构造函数的 prototype 属性是否出如今对象的原型链中的任何位置。
第二种方式能够经过对象的 constructor 属性来判断,对象的 constructor 属性指向该对象的构造函数,可是这种方式不是很安全,由于 constructor 属性能够被改写。
第三种方式,若是须要判断的是某个内置的引用类型的话,可使用 Object.prototype.toString() 方法来打印对象的[[Class]] 属性来进行判断。
// instanceof 运算符用于判断构造函数的 prototype 属性是否出如今对象的原型链中的任何位置。 // 实现: function myInstanceof(left, right) { let proto = Object.getPrototypeOf(left), // 获取对象的原型 prototype = right.prototype; // 获取构造函数的 prototype 对象 // 判断构造函数的 prototype 对象是否在对象的原型链上 while (true) { if (!proto) return false; if (proto === prototype) return true; proto = Object.getPrototypeOf(proto); } }
// (1)首先建立了一个新的空对象 // (2)设置原型,将对象的原型设置为函数的 prototype 对象。 // (3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性) // (4)判断函数的返回值类型,若是是值类型,返回建立的对象。若是是引用类型,就返回这个引用类型的对象。 // 实现: function objectFactory() { let newObject = null, constructor = Array.prototype.shift.call(arguments), result = null; // 参数判断 if (typeof constructor !== "function") { console.error("type error"); return; } // 新建一个空对象,对象的原型为构造函数的 prototype 对象 newObject = Object.create(constructor.prototype); // 将 this 指向新建对象,并执行函数 result = constructor.apply(newObject, arguments); // 判断返回对象 let flag = result && (typeof result === "object" || typeof result === "function"); // 判断返回结果 return flag ? result : newObject; } // 使用方法 // objectFactory(构造函数, 初始化参数);
hasOwnProperty
全部继承了 Object 的对象都会继承到 hasOwnProperty 方法。这个方法能够用来检测一个对象是否含有特定的自身属性,和in 运算符不一样,该方法会忽略掉那些从原型链上继承到的属性。
JSON 是一种基于文本的轻量级的数据交换格式。它能够被任何的编程语言读取和做为数据格式来传递。
在项目开发中,咱们使用 JSON 做为先后端数据交换的方式。在前端咱们经过将一个符合 JSON 格式的数据结构序列化为 JSON 字符串,而后将它传递到后端,后端经过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现先后端数据的一个传递。
由于 JSON 的语法是基于 js 的,所以很容易将 JSON 和 js 中的对象弄混,可是咱们应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,好比说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,所以大多数的 js 对象是不符合 JSON 对象的格式的。
在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,一个是 JSON.stringify 函数,经过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。若是传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,咱们能够调用这个函数将数据对象转化为 JSON 格式的字符串。
另外一个函数 JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,若是传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当咱们从后端接收到 JSON 格式的字符串时,咱们能够经过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。
(1)选取页面全部 DOM 元素。在浏览器的控制台中可使用$$()方法来获取页面中相应的元素,这是现代浏览器提供的一个命令行 API 至关于 document.querySelectorAll 方法。
(2)循环遍历 DOM 元素
(3)给元素添加 outline 。因为渲染的 outline 是不在 CSS 盒模型中的,因此为元素添加 outline 并不会影响元素的大小和页面的布局。
(4)生成随机颜色函数。Math.random()*(1<<24) 能够获得 0~2^24 - 1 之间的随机数,由于获得的是一个浮点数,但咱们只须要整数部分,使用取反操做符 ~ 连续两次取反得到整数部分,而后再用 toString(16) 的方式,转换为一个十六进制的字符串。
js 的加载、解析和执行会阻塞页面的渲染过程,所以咱们但愿 js 脚本可以尽量的延迟加载,提升页面的渲染速度。
我了解到的几种方式是:
第一种方式是咱们通常采用的是将 js 脚本放在文档的底部,来使 js 脚本尽量的在最后来加载执行。
第二种方式是给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,而后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来讲最后是顺序执行的,可是在一些浏览器中可能不是这样。
第三种方式是给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,可是当脚本加载完成后当即执行 js 脚本,这个时候若是文档没有解析完成的话一样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,通常不会按照代码的顺序依次执行。
第四种方式是动态建立 DOM 标签的方式,咱们能够对文档的加载事件进行监听,当文档加载完成后再动态的建立 script 标签来引入 js 脚本。
我对 ajax 的理解是,它是一种异步通讯的方法,经过直接由 js 脚本向服务器发起 http 通讯,而后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。
建立一个 ajax 有这样几个步骤
首先是建立一个 XMLHttpRequest 对象。
而后在这个对象上使用 open 方法建立一个 http 请求,open 方法所须要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
在发起请求前,咱们能够为这个对象添加一些信息和监听函数。好比说咱们能够经过setRequestHeader 方法来为请求添加头信息。咱们还能够为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,咱们能够经过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,表明服务器返回的数据接收完成,这个时候咱们能够经过判断请求的状态,若是状态是 2xx 或者 304 的话则表明返回正常。这个时候咱们就能够经过 response 中的数据来对页面进行更新了。
当对象的属性和监听函数设置完成后,最后咱们调用 sent 方法来向服务器发起请求,能够传入参数做为发送的数据体。
浏览器的缓存机制指的是经过在一段时间内保留已接收到的 web 资源的一个副本,若是在资源的有效时间内,发起了对这个资源的再一次请求,那么浏览器会直接使用缓存的副本,而不是向服务器发起请求。使用 web 缓存能够有效地提升页面的打开速度,减小没必要要的网络带宽的消耗。
web 资源的缓存策略通常由服务器来指定,能够分为两种,分别是强缓存策略和协商缓存策略。
使用强缓存策略时,若是缓存资源有效,则直接使用缓存资源,没必要再向服务器发起请求。强缓存策略能够经过两种方式来设置,分别是 http 头信息中的 Expires 属性和 Cache-Control 属性。
服务器经过在响应头中添加 Expires 属性,来指定资源的过时时间。在过时时间之内,该资源能够被缓存使用,没必要再向服务器发送请求。这个时间是一个绝对时间,它是服务器的时间,所以可能存在这样的问题,就是客户端的时间和服务器端的时间不一致,或者用户能够对客户端时间进行修改的状况,这样就可能会影响缓存命中的结果。
Expires 是 http1.0 中的方式,由于它的一些缺点,在 http 1.1 中提出了一个新的头部属性就是 Cache-Control 属性,它提供了对资源的缓存的更精确的控制。它有不少不一样的值,经常使用的好比咱们能够经过设置 max-age 来指定资源可以被缓存的时间的大小,这是一个相对的时间,它会根据这个时间的大小和资源第一次请求时的时间来计算出资源过时的时间,所以相对于 Expires来讲,这种方式更加有效一些。
经常使用的还有好比 private ,用来规定资源只能被客户端缓存,不可以代理服务器所缓存。还有如 no-store ,用来指定资源不可以被缓存,no-cache 表明该资源可以被缓存,可是当即失效,每次都须要向服务器发起请求。
通常来讲只须要设置其中一种方式就能够实现强缓存策略,当两种方式一块儿使用时,Cache-Control 的优先级要高于 Expires 。
使用协商缓存策略时,会先向服务器发送一个请求,若是资源没有发生修改,则返回一个 304 状态,让浏览器使用本地的缓存副本。若是资源发生了修改,则返回修改后的资源。协商缓存也能够经过两种方式来设置,分别是 http 头信息中的 Etag 和 Last-Modified 属性。
服务器经过在响应头中添加 Last-Modified 属性来指出资源最后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加一个 If-Modified-Since 的属性,属性值为上一次资源返回时的 Last-Modified 的值。当请求发送到服务器后服务器会经过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否作了修改。若是资源没有修改,那么返回 304 状态,让客户端使用本地的缓存。若是资源已经被修改了,则返回修改后的资源。使用这种方法有一个缺点,就是 Last-Modified 标注的最后修改时间只能精确到秒级,若是某些文件在1秒钟之内,被修改屡次的话,那么文件已将改变了可是 Last-Modified 却没有改变,这样会形成缓存命中的不许确。
由于 Last-Modified 的这种可能发生的不许确性,http 中提供了另一种方式,那就是 Etag 属性。服务器在返回资源的时候,在头信息中添加了 Etag 属性,这个属性是资源生成的惟一标识符,当资源发生改变的时候,这个值也会发生改变。在下一次资源请求时,浏览器会在请求头中添加一个 If-None-Match 属性,这个属性的值就是上次返回的资源的 Etag 的值。服务接收到请求后会根据这个值来和资源当前的 Etag 的值来进行比较,以此来判断资源是否发生改变,是否须要返回资源。经过这种方式,比 Last-Modified 的方式更加精确。
当 Last-Modified 和 Etag 属性同时出现的时候,Etag 的优先级更高。使用协商缓存的时候,服务器须要考虑负载平衡的问题,所以多个服务器上资源的 Last-Modified 应该保持一致,由于每一个服务器上 Etag 的值都不同,所以在考虑负载平衡时,最好不要设置 Etag 属性。
强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一块儿合做使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,若是命中则直接使用资源。若是不命中则根据头信息向服务器发起请求,使用协商缓存,若是协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,若是协商缓存不命中,则浏览器返回最新的资源给浏览器。
1.在 ajax 发送请求前加上 anyAjaxObj.setRequestHeader("If-Modified-Since","0")。
2.在 ajax 发送请求前加上 anyAjaxObj.setRequestHeader("Cache-Control","no-cache")。
3.在 URL 后面加上一个随机数: "fresh=" + Math.random();。
4.在 URL 后面加上时间戳:"nowtime=" + new Date().getTime();。
5.若是是使用 jQuery,直接这样就能够了$.ajaxSetup({cache:false})。这样页面的全部 ajax 都会执行这条语句就是不须要保存缓存记录。
同步指的是当一个进程在执行某个请求的时候,若是这个请求须要等待一段时间才能返回,那么这个进程会一直等待下去,直到消息返
回为止再继续向下执行。
异步指的是当一个进程在执行某个请求的时候,若是这个请求须要等待一段时间才能返回,这个时候进程会继续往下执行,不会阻塞等
待消息的返回,当消息返回时系统再通知进程进行处理。
我对浏览器的同源政策的理解是,一个域下的 js 脚本在未经容许的状况下,不可以访问另外一个域的内容。这里的同源的指的是两个域的协议、域名、端口号必须相同,不然则不属于同一个域。
同源政策主要限制了三个方面
第一个是当前域下的 js 脚本不可以访问其余域下的 cookie、localStorage 和 indexDB。
第二个是当前域下的 js 脚本不可以操做访问操做其余域下的 DOM。
第三个是当前域下 ajax 没法发送跨域请求。
同源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并非对浏览器的限制,对于通常的 img、或者script 脚本请求都不会有跨域的限制,这是由于这些操做都不会经过响应结果来进行可能出现安全问题的操做。
解决跨域的方法咱们能够根据咱们想要实现的目的来划分。
首先咱们若是只是想要实现主域名下的不一样子域名的跨域操做,咱们可使用设置 document.domain 来解决。
(1)将 document.domain 设置为主域名,来实现相同子域名的跨域操做,这个时候主域名下的 cookie 就可以被子域名所访问。同时若是文档中含有主域名相同,子域名不一样的 iframe 的话,咱们也能够对这个 iframe 进行操做。
若是是想要解决不一样跨域窗口间的通讯问题,好比说一个页面想要和页面的中的不一样源的 iframe 进行通讯的问题,咱们可使用 location.hash 或者 window.name 或者 postMessage 来解决。
(2)使用 location.hash 的方法,咱们能够在主页面动态的修改 iframe 窗口的 hash 值,而后在 iframe 窗口里实现监听函数来实现这样一个单向的通讯。由于在 iframe 是没有办法访问到不一样源的父级窗口的,因此咱们不能直接修改父级窗口的 hash 值来实现通讯,咱们能够在 iframe 中再加入一个 iframe ,这个 iframe 的内容是和父级页面同源的,因此咱们能够 window.parent.parent 来修改最顶级页面的 src,以此来实现双向通讯。
(3)使用 window.name 的方法,主要是基于同一个窗口中设置了 window.name 后不一样源的页面也能够访问,因此不一样源的子页面能够首先在 window.name 中写入数据,而后跳转到一个和父级同源的页面。这个时候级页面就能够访问同源的子页面中 window.name 中的数据了,这种方式的好处是能够传输的数据量大。
(4)使用 postMessage 来解决的方法,这是一个 h5 中新增的一个 api。经过它咱们能够实现多窗口间的信息传递,经过获取到指定窗口的引用,而后调用 postMessage 来发送信息,在窗口中咱们经过对 message 信息的监听来接收信息,以此来实现不一样源间的信息交换。
若是是像解决 ajax 没法提交跨域请求的问题,咱们可使用 jsonp、cors、websocket 协议、服务器代理来解决问题。
(5)使用 jsonp 来实现跨域请求,它的主要原理是经过动态构建 script 标签来实现跨域请求,由于浏览器对 script 标签的引入没有跨域的访问限制 。经过在请求的 url 后指定一个回调函数,而后服务器在返回数据的时候,构建一个 json 数据的包装,这个包装就是回调函数,而后返回给前端,前端接收到数据后,由于请求的是脚本文件,因此会直接执行,这样咱们先前定义好的回调函数就能够被调用,从而实现了跨域请求的处理。这种方式只能用于 get 请求。
(6)使用 CORS 的方式,CORS 是一个 W3C 标准,全称是"跨域资源共享"。CORS 须要浏览器和服务器同时支持。目前,全部浏览器都支持该功能,所以咱们只须要在服务器端配置就行。浏览器将 CORS 请求分红两类:简单请求和非简单请求。对于简单请求,浏览器直接发出 CORS 请求。具体来讲,就是会在头信息之中,增长一个 Origin 字段。Origin 字段用来讲明本次请求来自哪一个源。服务器根据这个值,决定是否赞成此次请求。对于若是 Origin 指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin 字段,就知道出错了,从而抛出一个错误,ajax 不会收到响应信息。若是成功的话会包含一些以 Access-Control- 开头的字段。
非简单请求,浏览器会先发出一次预检请求,来判断该域名是否在服务器的白名单中,若是收到确定回复后才会发起请求。
(7)使用 websocket 协议,这个协议没有同源限制。
(8)使用服务器来代理跨域的访问请求,就是有跨域的请求操做时发送请求给后端,让后端代为请求,而后最后将获取的结果发返回。
一个元素的拖拽过程,咱们能够分为三个步骤,第一步是鼠标按下目标元素,第二步是鼠标保持按下的状态移动鼠标,第三步是鼠标抬起,拖拽过程结束。
这三步分别对应了三个事件,mousedown 事件,mousemove 事件和 mouseup 事件。只有在鼠标按下的状态移动鼠标咱们才会执行拖拽事件,所以咱们须要在 mousedown 事件中设置一个状态来标识鼠标已经按下,而后在 mouseup 事件中再取消这个状态。在 mousedown 事件中咱们首先应该判断,目标元素是否为拖拽元素,若是是拖拽元素,咱们就设置状态而且保存这个时候鼠标的位置。而后在 mousemove 事件中,咱们经过判断鼠标如今的位置和之前位置的相对移动,来肯定拖拽元素在移动中的坐标。
最后 mouseup 事件触发后,清除状态,结束拖拽事件。
个人理解是 cookie 是服务器提供的一种用于维护会话状态信息的数据,经过服务器发送到浏览器,浏览器保存在本地,当下一次有同源的请求时,将保存的 cookie 值添加到请求头部,发送给服务端。这能够用来实现记录用户登陆状态等功能。cookie 通常能够存储 4k 大小的数据,而且只可以被同源的网页所共享访问。
服务器端可使用 Set-Cookie 的响应头部来配置 cookie 信息。一条cookie 包括了5个属性值 expires、domain、path、secure、HttpOnly。其中 expires 指定了 cookie 失效的时间,domain 是域名、path是路径,domain 和 path 一块儿限制了 cookie 可以被哪些 url 访问。secure 规定了 cookie 只能在确保安全的状况下传输,HttpOnly 规定了这个 cookie 只能被服务器访问,不能使用 js 脚本访问。
在发生 xhr 的跨域请求的时候,即便是同源下的 cookie,也不会被自动添加到请求头部,除非显示地规定。
我对模块的理解是,一个模块是实现一个特定功能的一组方法。在最开始的时候,js 只实现一些简单的功能,因此并无模块的概念,但随着程序愈来愈复杂,代码的模块化开发变得愈来愈重要。
因为函数具备独立做用域的特色,最原始的写法是使用函数来做为模块,几个函数做为一个模块,可是这种方式容易形成全局变量的污染,而且模块间没有联系。
后面提出了对象写法,经过将函数做为一个对象的方法来实现,这样解决了直接使用函数做为模块的一些缺点,可是这种办法会暴露全部的全部的模块成员,外部代码能够修改内部属性的值。
如今最经常使用的是当即执行函数的写法,经过利用闭包来实现模块私有做用域的创建,同时不会对全局做用域形成污染。
js 中如今比较成熟的有四种模块加载方案。
第一种是 CommonJS 方案,它经过 require 来引入模块,经过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,由于在服务端文件都存储在本地磁盘,因此读取很是快,因此以同步的方式加载没有问题。但若是是在浏览器端,因为模块的加载是使用网络请求,所以使用异步加载的方式更加合适。
第二种是 AMD 方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,全部依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。
第三种是 CMD 方案,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。它和 require.js的区别在于模块定义时对依赖的处理不一样和对依赖模块的执行时机的处理不一样。参考60
第四种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块。这种方案和上面三种方案都不一样。参考 61。
它们之间的主要区别有两个方面。
(1)第一个方面是在模块定义时对依赖的处理不一样。AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块。而 CMD 推崇
就近依赖,只有在用到某个模块的时候再去 require。
(2)第二个方面是对依赖模块的执行时机处理不一样。首先 AMD 和 CMD 对于模块的加载方式都是异步加载,不过它们的区别在于模块的执行时机,AMD 在依赖模块加载完成后就直接执行依赖模块,依赖模块的执行顺序和咱们书写的顺序不必定一致。而 CMD在依赖模块加载完成后并不执行,只是下载而已,等到全部的依赖模块都加载好后,进入回调函数逻辑,遇到 require 语句的时候才执行对应的模块,这样模块的执行顺序就和咱们书写的顺序保持一致了。
// CMD define(function(require, exports, module) { var a = require("./a"); a.doSomething(); // 此处略去 100 行 var b = require("./b"); // 依赖能够就近书写 b.doSomething(); // ... }); // AMD 默认推荐 define(["./a", "./b"], function(a, b) { // 依赖必须一开始就写好 a.doSomething(); // 此处略去 100 行 b.doSomething(); // ... });
1.CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运行机制与 CommonJS 不同。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
2.CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。CommonJS 模块就是对象,即在输入时是先加载整个模块,生成一个对象,而后再从这个对象上面读取方法,这种加载称为“运行时加载”。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
require.js 的核心原理是经过动态建立 script 脚原本异步引入模块,而后对每一个脚本的 load 事件进行监听,若是每一个脚本都加载完成了,再调用回调函数。
Promise 对象是异步编程的一种解决方案,最先由社区提出。Promises/A+ 规范是 JavaScript Promise 的标准,规定了一个 Promise 所必须具备的特性。
Promise 是一个构造函数,接收一个函数做为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是 pending、resolved 和 rejected,分别表明了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者 rejected 状态,而且状态一经改变,就凝固了,没法再被改变了。状态的改变是经过 resolve() 和 reject() 函数来实现的,咱们能够在异步操做结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法能够为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
在我看来 ES6 新添加的 class 只是为了补充 js 中缺乏的一些面向对象语言的特性,但本质上来讲它只是一种语法糖,不是一个新的东西,其背后仍是原型继承的思想。经过加入 class 能够有利于咱们更好的组织代码。
在 class 中添加的方法,实际上是添加在类的原型上的。
document.write 的内容会代替整个文档内容,会重写整个页面。
innerHTML 的内容只是替代指定元素的内容,只会重写页面中的部份内容。
(1)建立新节点
createDocumentFragment(node); createElement(node); createTextNode(text);
(2)添加、移除、替换、插入
appendChild(node) removeChild(node) replaceChild(new,old) insertBefore(new,old)
(3)查找
getElementById(); getElementsByName(); getElementsByTagName(); getElementsByClassName(); querySelector(); querySelectorAll();
(4)属性操做
getAttribute(key); setAttribute(key, value); hasAttribute(key); removeAttribute(key);
对于这样一个 HTML 元素:
innerHTML:内部 HTML,content
;
outerHTML:外部 HTML,
它们的做用如出一辙,区别仅在于传入参数的形式的不一样。
apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合能够为数组,也能够为类数组,apply 方法把这个集合中的元素做为参数传递给被调用的函数。
call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是表明函数体内的 this 指向,从第二个参数开始日后,每一个参数被依次传入函数。
常见的类数组转换为数组的方法有这样几种:
(1)经过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
(2)经过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike, 0);
(3)经过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);
(4)经过 Array.from 方法来实现转换
Array.from(arrayLike);
数组和字符串的转换方法:toString()、toLocalString()、join() 其中 join() 方法能够指定转换为字符串时的分隔符。
数组尾部操做的方法 pop() 和 push(),push 方法能够传入多个参数。
数组首部操做的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法能够传入一个函数来进行比较,传入先后两个值,若是返回值为正数,则交换两个参数的位置。
数组链接的方法 concat() ,返回的是拼接好的数组,不影响原数组。
数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。
数组插入方法 splice(),影响原数组查找特定项的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法
数组归并方法 reduce() 和 reduceRight() 方法
fill() 方法用一个固定值填充一个数组中从起始索引到终止索引内的所有元素。不包括终止索引。
fill 方法接受三个参数 value,start 以及 end,start 和 end 参数是可选的,其默认值分别为 0 和 this 对象的 length 属性值。
尾后逗号 (有时叫作“终止逗号”)在向 JavaScript 代码添加元素、参数、属性时十分有用。若是你想要添加新的属性,而且上一行已经使用了尾后逗号,你能够仅仅添加新的一行,而不须要修改上一行。这使得版本控制更加清晰,以及代码维护麻烦更少。
JavaScript 一开始就支持数组字面值中的尾后逗号,随后向对象字面值(ECMAScript 5)中添加了尾后逗号。最近(ECMAScript 2017),又将其添加到函数参数中。可是 JSON 不支持尾后逗号。
若是使用了多于一个尾后逗号,会产生间隙。 带有间隙的数组叫作稀疏数组(密致数组没有间隙)。稀疏数组的长度为逗号的数量。
变量提高的表现是,不管咱们在函数中何处位置声明的变量,好像都被提高到了函数的首部,咱们能够在变量声明前访问到而不会报错。
形成变量声明提高的本质缘由是 js 引擎在代码执行前有一个解析的过程,建立了执行上下文,初始化了一些代码执行时须要用到的对象。当咱们访问一个变量时,咱们会到当前执行上下文中的做用域链中去查找,而做用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、全部的函数和变量声明,这个对象的是在代码解析的时候建立的。这就是会出现变量声明提高的根本缘由。
1.使用位运算代替一些简单的四则运算。
2.避免使用过深的嵌套循环。
3.不要使用未定义的变量。
4.当须要屡次访问数组长度时,能够用变量保存起来,避免每次都会去进行属性查找。
v8 的垃圾回收机制基于分代回收机制,这个机制又基于世代假说,这个假说有两个特色,一是新生的对象容易早死,另外一个是不死的对象会活得更久。基于这个假说,v8 引擎将内存分为了新生代和老生代。
新建立的对象或者只经历过一次的垃圾回收的对象被称为新生代。经历过屡次垃圾回收的对象被称为老生代。
新生代被分为 From 和 To 两个空间,To 通常是闲置的。当 From 空间满了的时候会执行Scavenge 算法进行垃圾回收。当咱们执行垃圾回收算法的时候应用逻辑将会中止,等垃圾回收结束后再继续执行。这个算法分为三步:
(1)首先检查 From 空间的存活对象,若是对象存活则判断对象是否知足晋升到老生代的条件,若是知足条件则晋升到老生代。若是不知足条件则移动 To 空间。
(2)若是对象不存活,则释放对象的空间。
(3)最后将 From 空间和 To 空间角色进行交换。
新生代对象晋升到老生代有两个条件:
(1)第一个是判断是对象否已经通过一次 Scavenge 回收。若经历过,则将对象从 From 空间复制到老生代中;若没有经历,则复制到 To 空间。
(2)第二个是 To 空间的内存使用占比是否超过限制。当对象从 From 空间复制到 To 空间时,若 To 空间使用超过 25%,则对象直接晋升到老生代中。设置 25% 的缘由主要是由于算法结束后,两个空间结束后会交换位置,若是 To 空间的内存过小,会影响后续的内存分配。
老生代采用了标记清除法和标记压缩法。标记清除法首先会对内存中存活的对象进行标记,标记结束后清除掉那些没有标记的对象。因为标记清除后会形成不少的内存碎片,不便于后面的内存分配。因此了解决内存碎片的问题引入了标记压缩法。
因为在进行垃圾回收的时候会暂停应用的逻辑,对于新生代方法因为内存小,每次停顿的时间不会太长,但对于老生代来讲每次垃圾回收的时间长,停顿会形成很大的影响。 为了解决这个问题 V8 引入了增量标记的方法,将一次停顿进行的过程分为了多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行。
第一种状况是咱们因为使用未声明的变量,而意外的建立了一个全局变量,而使这个变量一直留在内存中没法被回收。
第二种状况是咱们设置了 setInterval 定时器,而忘记取消它,若是循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而没法被回收。
第三种状况是咱们获取一个 DOM 元素的引用,然后面这个元素被删除,因为咱们一直保留了对这个元素的引用,因此它也没法被回收。
第四种状况是不合理的使用闭包,从而致使某些变量一直被留在内存当中。
经过使用 pushState + ajax 实现浏览器无刷新前进后退,当一次 ajax 调用成功后咱们将一条 state 记录加入到 history对象中。一条 state 记录包含了 url、title 和 content 属性,在 popstate 事件中能够获取到这个 state 对象,咱们可使用 content 来传递数据。最后咱们经过对 window.onpopstate 事件监听来响应浏览器的前进后退操做。
使用 pushState 来实现有两个问题,一个是打开首页时没有记录,咱们可使用 replaceState 来将首页的记录替换,另外一个问题是当一个页面刷新的时候,仍然会向服务器端请求数据,所以若是请求的 url 须要后端的配合将其重定向到一个页面。
this === window ? 'browser' : 'node';
经过判断 Global 对象是否为 window,若是不为 window,当前脚本没有运行在浏览器中。
Promise 对象是异步编程的一种解决方案,最先由社区提出。Promises/A+ 规范是 JavaScript Promise 的标准,规定了一个 Promise 所必须具备的特性。
Promise 是一个构造函数,接收一个函数做为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是 pending、resolved 和 rejected,分别表明了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者 rejected 状态,而且状态一经改变,就凝固了,没法再被改变了。状态的改变是经过 resolve() 和 reject() 函数来实现的,咱们能够在异步操做结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法能够为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
移动端点击有 300ms 的延迟是由于移动端会有双击缩放的这个操做,所以浏览器在 click 以后要等待 300ms,看用户有没有下一次点击,来判断此次操做是否是双击。
(1)什么是前端路由?
前端路由就是把不一样路由对应不一样的内容或页面的任务交给前端来作,以前是经过服务端根据 url 的不一样返回不一样的页面实现的。
(2)何时使用前端路由?
在单页面应用,大部分页面结构不变,只改变部份内容的使用
(3)前端路由有什么优势和缺点?
优势:用户体验好,不须要每次都从服务器所有获取,快速展示给用户
缺点:单页面没法记住以前滚动的位置,没法在前进,后退的时候记住滚动的位置
前端路由一共有两种实现方式,一种是经过 hash 的方式,一种是经过使用 pushState 的方式。
发布订阅模式其实属于广义上的观察者模式
在观察者模式中,观察者须要直接订阅目标事件。在目标发出内容改变的事件后,直接接收事件并做出响应。
而在发布订阅模式中,发布者和订阅者之间多了一个调度中心。调度中心一方面从发布者接收事件,另外一方面向订阅者发布事件,订阅者须要在调度中心中订阅事件。经过调度中心实现了发布者和订阅者关系的解耦。使用发布订阅者模式更利于咱们代码的可维护性。
检测浏览器版本一共有两种方式:
一种是检测 window.navigator.userAgent 的值,但这种方式很不可靠,由于 userAgent 能够被改写,而且早期的浏览器如 ie,会经过假装本身的 userAgent 的值为 Mozilla 来躲过服务器的检测。
第二种方式是功能检测,根据每一个浏览器独有的特性来进行判断,如 ie 下独有的 ActiveXObject。
Polyfill 指的是用于实现浏览器并不支持的原生 API 的代码。
好比说 querySelectorAll 是不少现代浏览器都支持的原生 Web API,可是有些古老的浏览器并不支持,那么假设有人写了一段代码来实现这个功能使这些浏览器也支持了这个功能,那么这就能够成为一个 Polyfill。
一个 shim 是一个库,有本身的 API,而不是单纯实现原生不支持的 API。
// String.lastIndexOf() 方法返回指定值(本例中的'.')在调用该方法的字符串中最后出现的位置,若是没找到则返回 -1。 // 对于 'filename' 和 '.hiddenfile' ,lastIndexOf 的返回值分别为 0 和 -1 无符号右移操做符(>>>) 将 -1 转换为 4294967295 ,将 -2 转换为 4294967294 ,这个方法能够保证边缘状况时文件名不变。 // String.prototype.slice() 从上面计算的索引处提取文件的扩展名。若是索引比文件名的长度大,结果为""。 function getFileExtension(filename) { return filename.slice(((filename.lastIndexOf(".") - 1) >>> 0) + 2); }
函数防抖是指在事件被触发 n 秒后再执行回调,若是在这 n 秒内事件又被触发,则从新计时。这可使用在一些点击请求的事件上,避免由于用户的屡次点击向后端发送屡次请求。
函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,若是在同一个单位时间内某事件被触发屡次,只有一次能生效。节流可使用在 scroll 函数的事件监听上,经过事件节流来下降事件调用的频率。
使用双等号进行相等判断时,若是两边的类型不一致,则会进行强制类型转化后再进行比较。
使用三等号进行相等判断时,若是两边的类型不一致时,不会作强制类型准换,直接返回 false。
使用 Object.is 来进行相等判断时,通常状况下和三等号的判断相同,它处理了一些特殊的状况,好比 -0 和 +0 再也不相等,两个 NaN 认定为是相等的。
encodeURI 是对整个 URI 进行转义,将 URI 中的非法字符转换为合法字符,因此对于一些在 URI 中有特殊意义的字符不会进行转义。
encodeURIComponent 是对 URI 的组成部分进行转义,因此一些特殊字符也会获得转义。
escape 和 encodeURI 的做用相同,不过它们对于 unicode 编码为 0xff 以外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式,再在每一个字节前加上 %。
Unicode 是一种字符集合,如今可容纳 100 多万个字符。每一个字符对应一个不一样的 Unicode 编码,它只规定了符号的二进制代码,却没有规定这个二进制代码在计算机中如何编码传输。
UTF-8 是一种对 Unicode 的编码方式,它是一种变长的编码方式,能够用 1~4 个字节来表示一个字符。
由于 js 是单线程运行的,在代码执行的时候,经过将不一样函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码的时候,若是遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其余任务。当异步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不一样的另外一个任务队列中等待执行。任务队列能够分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务能够执行,若是有就将微任务队首的事件压入栈中执行。当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。
微任务包括了 promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。
宏任务包括了 script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操做、UI 渲染等。
浅拷贝指的是将一个对象的属性值复制到另外一个对象,若是有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,所以两个对象会有同一个引用类型的引用。浅拷贝可使用 Object.assign 和展开运算符来实现。
深拷贝相对浅拷贝而言,若是遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,所以对象得到的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可使用 JSON 的两个函数来实现,可是因为 JSON 的对象格式比 js 的对象格式更加严格,因此若是属性值里边出现函数或者 Symbol 类型的值时,会转换失败。
call 函数的实现步骤:
1.判断调用对象是否为函数,即便咱们是定义在函数的原型上的,可是可能出现使用 call 等方式调用的状况。
2.判断传入上下文对象是否存在,若是不存在,则设置为 window 。
3.处理传入的参数,截取第一个参数后的全部参数。
4.将函数做为上下文对象的一个属性。
5.使用上下文对象来调用这个方法,并保存返回结果。
6.删除刚才新增的属性。
7.返回结果。
apply 函数的实现步骤:
1.判断调用对象是否为函数,即便咱们是定义在函数的原型上的,可是可能出现使用 call 等方式调用的状况。
2.判断传入上下文对象是否存在,若是不存在,则设置为 window 。
3.将函数做为上下文对象的一个属性。
4.判断参数值是否传入
4.使用上下文对象来调用这个方法,并保存返回结果。
5.删除刚才新增的属性
6.返回结果
bind 函数的实现步骤:
1.判断调用对象是否为函数,即便咱们是定义在函数的原型上的,可是可能出现使用 call 等方式调用的状况。
2.保存当前函数的引用,获取其他传入参数值。
3.建立一个函数返回
4.函数内部使用 apply 来绑定函数调用,须要判断函数做为构造函数的状况,这个时候须要传入当前函数的 this 给 apply 调用,其他状况都传入指定的上下文对象。
// 函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。 function curry(fn, args) { // 获取函数须要的参数长度 let length = fn.length; args = args || []; return function() { let subArgs = args.slice(0); // 拼接获得现有的全部参数 for (let i = 0; i < arguments.length; i++) { subArgs.push(arguments[i]); } // 判断参数的长度是否已经知足函数所需参数的长度 if (subArgs.length >= length) { // 若是知足,执行函数 return fn.apply(this, subArgs); } else { // 若是不知足,递归返回科里化的函数,等待参数的传入 return curry.call(this, fn, subArgs); } }; } // es6 实现 function curry(fn, ...args) { return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args); }
当计算机计算 0.1+0.2 的时候,实际上计算的是这两个数字在计算机里所存储的二进制,0.1 和 0.2 在转换为二进制表示的时候会出现位数无限循环的状况。js 中是以 64 位双精度格式来存储数字的,只有 53 位的有效数字,超过这个长度的位数会被截取掉这样就形成了精度丢失的问题。这是第一个会形成精度丢失的地方。在对两个以 64 位双精度格式的数据进行计算的时候,首先会进行对阶的处理,对阶指的是将阶码对齐,也就是将小数点的位置对齐后,再进行计算,通常是小阶向大阶对齐,所以小阶的数在对齐的过程当中,有效数字会向右移动,移动后超过有效位数的位会被截取掉,这是第二个可能会出现精度丢失的地方。当两个数据阶码对齐后,进行相加运算后,获得的结果可能会超过 53 位有效数字,所以超过的位数也会被截取掉,这是可能发生精度丢失的第三个地方。
对于这样的状况,咱们能够将其转换为整数后再进行运算,运算后再转换为对应的小数,以这种方式来解决这个问题。
咱们还能够将两个数相加的结果和右边相减,若是相减的结果小于一个极小数,那么咱们就能够认定结果是相等的,这个极小数可使用 es6 的 Number.EPSILON
XSS 攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者经过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。
XSS 的本质是由于网站没有对恶意代码进行过滤,与正常的代码混合在一块儿了,浏览器没有办法分辨哪些脚本是可信的,从而致使了恶意代码的执行。
XSS 通常分为存储型、反射型和 DOM 型。
存储型指的是恶意代码提交到了网站的数据库中,当用户请求数据的时候,服务器将其拼接为 HTML 后返回给了用户,从而致使了恶意代码的执行。
反射型指的是攻击者构建了特殊的 URL,当服务器接收到请求后,从 URL 中获取数据,拼接到 HTML 后返回,从而致使了恶意代码的执行。
DOM 型指的是攻击者构建了特殊的 URL,用户打开网站后,js 脚本从 URL 中获取数据,从而致使了恶意代码的执行。
XSS 攻击的预防能够从两个方面入手,一个是恶意代码提交的时候,一个是浏览器执行恶意代码的时候。
对于第一个方面,若是咱们对存入数据库的数据都进行的转义处理,可是一个数据可能在多个地方使用,有的地方可能不须要转义,因为咱们没有办法判断数据最后的使用场景,因此直接在输入端进行恶意代码的处理,实际上是不太可靠的。
所以咱们能够从浏览器的执行来进行预防,一种是使用纯前端的方式,不用服务器端拼接后返回。另外一种是对须要插入到 HTML 中的代码作好充分的转义。对于 DOM 型的攻击,主要是前端脚本的不可靠而形成的,咱们对于数据获取渲染和字符串拼接的时候应该对可能出现的恶意代码状况进行判断。
还有一些方式,好比使用 CSP ,CSP 的本质是创建一个白名单,告诉浏览器哪些外部资源能够加载和执行,从而防止恶意代码的注入攻击。
还能够对一些敏感信息进行保护,好比 cookie 使用 http-only ,使得脚本没法获取。也可使用验证码,避免脚本假装成用户执行一些操做。
CSRF 攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,而后该网站向被攻击网站发送跨站请求。若是用户在被攻击网站中保存了登陆状态,那么攻击者就能够利用这个登陆状态,绕事后台的用户验证,冒充用户向服务器执行一些操做。
CSRF 攻击的本质是利用了 cookie 会在同源请求中携带发送给服务器的特色,以此来实现用户的冒充。
通常的 CSRF 攻击类型有三种:
第一种是 GET 类型的 CSRF 攻击,好比在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提
交。
第二种是 POST 类型的 CSRF 攻击,好比说构建一个表单,而后隐藏它,当用户进入页面时,自动提交这个表单。
第三种是连接类型的 CSRF 攻击,好比说在 a 标签的 href 属性里构建一个请求,而后诱导用户去点击。
CSRF 能够用下面几种方法来防御:
第一种是同源检测的方法,服务器根据 http 请求头中 origin 或者 referer 信息来判断请求是否为容许访问的站点,从而对请求进行过滤。当 origin 或者 referer 信息都不存在的时候,直接阻止。这种方式的缺点是有些状况下 referer 能够被伪造。还有就是咱们这种方法同时把搜索引擎的连接也给屏蔽了,因此通常网站会容许搜索引擎的页面请求,可是相应的页面请求这种请求方式也可能被攻击者给利用。
第二种方法是使用 CSRF Token 来进行验证,服务器向用户返回一个随机数 Token ,当网站再次发起请求时,在请求参数中加入服务器端返回的 token ,而后服务器对这个 token 进行验证。这种方法解决了使用 cookie 单一验证方式时,可能会被冒用的问题,可是这种方法存在一个缺点就是,咱们须要给网站中的全部请求都添加上这个 token,操做比较繁琐。还有一个问题是通常不会只有一台网站服务器,若是咱们的请求通过负载平衡转移到了其余的服务器,可是这个服务器的 session 中没有保留这个 token 的话,就没有办法验证了。这种状况咱们能够经过改变 token 的构建方式来解决。
第三种方式使用双重 Cookie 验证的办法,服务器在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串,而后当用户再次向服务器发送请求的时候,从 cookie 中取出这个字符串,添加到 URL 参数中,而后服务器经过对 cookie 中的数据和参数中的数据进行比较,来进行验证。使用这种方式是利用了攻击者只能利用 cookie,可是不能访问获取 cookie 的特色。而且这种方法比 CSRF Token 的方法更加方便,而且不涉及到分布式访问的问题。这种方法的缺点是若是网站存在 XSS 漏洞的,那么这种方式会失效。同时这种方式不能作到子域名的隔离。
第四种方式是使用在设置 cookie 属性的时候设置 Samesite ,限制 cookie 不能做为被第三方使用,从而能够避免被攻击者利用。Samesite 一共有两种模式,一种是严格模式,在严格模式下 cookie 在任何状况下都不可能做为第三方 Cookie 使用,在宽松模式下,cookie 能够被请求是 GET 请求,且会发生页面跳转的请求所使用。
我把JavaScript面试题整理成两份文档,有一份是按分类来整理,一共有19个内容,具体下图所示:
另外一份是有191道题目,是文中题目的延续,整理的文中的面试题在这份文档中,下图所示:
如何获取这两份优质的资料呢?
快速入手通道:点击这个,免费下载!诚意满满!!!
整理不易,以为有帮助的朋友能够帮忙点赞分享支持一下小编谢谢~