最近在整理 JavaScript 的时候发现遇到了不少面试中常见的面试题,本部分主要是做者在 Github 等各大论坛收录的 JavaScript 相关知识和一些相关面试题时所作的笔记,分享这份总结给你们,对你们对 JavaScript 的能够来一次全方位的检漏和排查,感谢原做者 CavsZhouyou 的付出,原文连接放在文章最下方,若是出现错误,但愿你们共同指出!更多往期面试整理可移步:javascript
若是文章和笔记能带您一丝帮助或者启发,请不要吝啬你的赞和收藏,你的确定是我前进的最大动力 😁css
js 一共有六种基本数据类型,分别是 Undefined、Null、Boolean、Number、String,还有在 ES6 中新增的 Symbol 类型, 表明建立后独一无二且不可变的数据类型,它的出现我认为主要是为了解决可能出现的全局变量冲突的问题。
涉及知识点:html
两种类型的区别是:存储位置不一样。 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,因此放入栈中存储。 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。若是存储在栈中,将会影响程序运行的性能;引用数据类型在 栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中得到实 体。
回答:前端
js 能够分为两种类型的值,一种是基本数据类型,一种是复杂数据类型。 基本数据类型....(参考1) 复杂数据类型指的是 Object 类型,全部其余的如 Array、Date 等数据类型均可以理解为 Object 类型的子类。 两种类型间的主要区别是它们的存储位置不一样,基本数据类型的值直接保存在栈中,而复杂数据类型的值保存在堆中,经过使用在栈中 保存对应的指针来获取堆中的值。
详细资料能够参考:
《JavaScript 有几种类型的值?》
《JavaScript 有几种类型的值?可否画一下它们的内存图;》vue
堆和栈的概念存在于数据结构中和操做系统内存中。 在数据结构中,栈中数据的存取方式为先进后出。而堆是一个优先队列,是按优先级来进行排序的,优先级能够按照大小来规定。彻底 二叉树是堆的一种实现方式。 在操做系统中,内存被分为栈区和堆区。 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操做方式相似于数据结构中的栈。 堆区内存通常由程序员分配释放,若程序员不释放,程序结束时可能由垃圾回收机制回收。
详细资料能够参考:
《什么是堆?什么是栈?他们之间有什么区别和联系?》html5
全部 typeof 返回值为 "object" 的对象(如数组)都包含一个内部属性 [[Class]](咱们能够把它看做一个内部的分类,而非 传统的面向对象意义上的类)。这个属性没法直接访问,通常经过 Object.prototype.toString(..) 来查看。例如: Object.prototype.toString.call( [1,2,3] ); // "[object Array]" Object.prototype.toString.call( /regex-literal/i ); // "[object RegExp]"
涉及知识点:java
全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。这里说的全局的对象是说在 全局做用域里的对象。全局做用域中的其余对象能够由用户的脚本建立或由宿主程序提供。 标准内置对象的分类 (1)值属性,这些全局属性返回一个简单值,这些值没有本身的属性和方法。 例如 Infinity、NaN、undefined、null 字面量 (2)函数属性,全局函数能够直接调用,不须要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。 例如 eval()、parseFloat()、parseInt() 等 (3)基本对象,基本对象是定义或使用其余对象的基础。基本对象包括通常对象、函数对象和错误对象。 例如 Object、Function、Boolean、Symbol、Error 等 (4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。 例如 Number、Math、Date (5)字符串,用来表示和操做字符串的对象。 例如 String、RegExp (6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array (7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。 例如 Map、Set、WeakMap、WeakSet (8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。 例如 SIMD 等 (9)结构化数据,这些对象用来表示和操做结构化的缓冲区数据,或使用 JSON 编码的数据。 例如 JSON 等 (10)控制抽象对象 例如 Promise、Generator 等 (11)反射 例如 Reflect、Proxy (12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。 例如 Intl、Intl.Collator 等 (13)WebAssembly (14)其余 例如 arguments
回答:node
js 中的内置对象主要指的是在程序执行前存在全局做用域里的由 js 定义的一些全局值属性、函数和用来实例化其余对象的构造函 数对象。通常咱们常常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构 造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。
详细资料能够参考:
《标准内置对象的分类》
《JS 全部内置对象属性和方法汇总》webpack
已在做用域中声明但尚未赋值的变量,是 undefined 的。相反,尚未在做用域中声明过的变量,是 undeclared 的。 对于 undeclared 变量的引用,浏览器会报引用错误,如 ReferenceError: b is not defined 。可是咱们能够使用 typ eof 的安全防范机制来避免报错,由于对于 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。
详细资料能够参考:
《JavaScript 深刻理解之 undefined 与 null》ios
由于 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 对象是经过引用来传递的,咱们建立的每一个新对象实体中并无一份属于本身的原型副本。当咱们修改原型时,与 之相关的对象也会继承这一改变。
详细资料能够参考:
《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() )中含有非数字字符,解析按从左到右的顺序,若是遇到非数字字符就中止。而转换(如 Nu mber ())不容许出现非数字字符,不然会失败并返回 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。
详细资料能够参考:
《JavaScript 字符串间的比较》
(1)使用 Number() 方法,前提是所包含的字符串不包含不合法字符。 (2)使用 parseInt() 方法,parseInt() 函数可解析一个字符串,并返回一个整数。还能够设置要解析的数字的基数。当基数的值为 0,或没有设置该参数时,parseInt() 会根据 string 来判断数字的基数。 (3)使用 parseFloat() 方法,该函数解析一个字符串参数并返回一个浮点数。 (4)使用 + 操做符的隐式转换。
详细资料能够参考:
《详解 JS 中 Number()、parseInt() 和 parseFloat() 的区别》
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}$/;
详细资料能够参考:
《前端表单验证经常使用的 15 个 JS 正则表达式》
《JS 经常使用正则汇总》
《JS - 生成随机数的方法汇总(不一样范围、类型的随机数)》
// (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; }
详细资料能够参考:
《Fisher and Yates 的原始版》
《javascript 实现数组随机排序?》
《JavaScript 学习笔记:数组随机排序》
咱们通常使用字面量的形式直接建立对象,可是这种建立方式对于建立大量类似对象的时候,会产生大量的重复代码。但 js 和通常的面向对象的语言不一样,在 ES6 以前它没有类的概念。可是咱们能够使用函数来进行模拟,从而产生出可复用的对象 建立方式,我了解到的方式有这么几种: (1)第一种是工厂模式,工厂模式的主要工做原理是用函数来封装建立对象的细节,从而经过调用函数来达到复用的目的。可是它有一个很大的问题就是建立出来的对象没法和某个类型联系起来,它只是简单的封装了复用代码,而没有创建起对象和类型间的关系。 (2)第二种是构造函数模式。js 中每个函数均可以做为构造函数,只要一个函数是经过 new 来调用的,那么咱们就能够把它称为构造函数。执行构造函数首先会建立一个对象,而后将对象的原型指向构造函数的 prototype 属性,而后将执行上下文中的 this 指向这个对象,最后再执行整个函数,若是返回值不是对象,则返回新建的对象。由于 this 的值指向了新建的对象,所以咱们能够使用 this 给对象赋值。构造函数模式相对于工厂模式的优势是,所建立的对象和构造函数创建起了联系,所以咱们能够经过原型来识别对象的类型。可是构造函数存在一个缺点就是,形成了没必要要的函数对象的建立,由于在 js 中函数也是一个对象,所以若是对象属性中若是包含函数的话,那么每次咱们都会新建一个函数对象,浪费了没必要要的内存空间,由于函数是全部的实例均可以通用的。 (3)第三种模式是原型模式,由于每个函数都有一个 prototype 属性,这个属性是一个对象,它包含了经过构造函数建立的全部实例都能共享的属性和方法。所以咱们能够使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来讲,解决了函数对象的复用问题。可是这种模式也存在一些问题,一个是没有办法经过传入参数来初始化值,另外一个是若是存在一个引用类型如 Array 这样的值,那么全部的实例将共享一个对象,一个实例对引用类型值的改变会影响全部的实例。 (4)第四种模式是组合使用构造函数模式和原型模式,这是建立自定义类型的最多见方式。由于构造函数模式和原型模式分开使用都存在一些问题,所以咱们能够组合使用这两种模式,经过构造函数来初始化对象的属性,经过原型对象来实现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,可是有一点不足的就是,由于使用了两种不一样的模式,因此对于代码的封装性不够好。 (5)第五种模式是动态原型模式,这一种模式将原型方法赋值的建立过程移动到了构造函数的内部,经过对属性是否存在的判断,能够实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。 (6)第六种模式是寄生构造函数模式,这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式同样,没法实现对象的识别。 嗯我目前了解到的就是这么几种方式。
详细资料能够参考:
《JavaScript 深刻理解之对象建立》
我了解的 js 中实现继承的几种方式有: (1)第一种是以原型链的方式来实现继承,可是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被全部的实例对象所共享,容易形成修改的混乱。还有就是在建立子类型的时候不能向超类型传递参数。 (2)第二种方式是使用借用构造函数的方式,这种方式是经过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,可是它存在的一个问题就是没法实现函数方法的复用,而且超类型原型定义的方法子类型也没有办法访问到。 (3)第三种方式是组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。经过借用构造函数的方式来实现类型的属性的继承,经过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,可是因为咱们是以超类型的实例来做为子类型的原型,因此调用了两次超类的构造函数,形成了子类型的原型中多了不少没必要要的属性。 (4)第四种方式是原型式继承,原型式继承的主要思路就是基于已有的对象来建立新的对象,实现的原理是,向函数中传入一个对象,而后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。 (5)第五种方式是寄生式继承,寄生式继承的思路是建立一个用于封装继承过程的函数,经过传入一个对象,而后复制一个对象的副本,而后对象进行扩展,最后返回这个对象。这个扩展的过程就能够理解是一种继承。这种继承的优势就是对一个简单对象实现继承,若是这个对象不是咱们的自定义类型时。缺点是没有办法实现函数的复用。 (6)第六种方式是寄生式组合继承,组合继承的缺点就是使用超类型的实例作为子类型的原型,致使添加了没必要要的原型属性。寄生式组合继承的方式是使用超类型的原型的副原本做为子类型的原型,这样就避免了建立没必要要的属性。
详细资料能够参考:
《JavaScript 深刻理解之继承》
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 + "."); };
做用域链的做用是保证对执行环境有权访问的全部变量和函数的有序访问,经过做用域链,咱们能够访问到外层环境的变量和 函数。 做用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中全部变量和函数的对象。做用域链的前 端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是做用域链的最后一个对象。 当咱们查找一个变量时,若是当前执行环境中没有找到,咱们能够沿着做用域链向后查找。 做用域链的建立过程跟执行上下文的创建有关....
详细资料能够参考:
《JavaScript 深刻理解之做用域链》
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向能够经过四种调用模 式来判断。
这四种方式,使用构造器调用模式的优先级最高,而后是 apply 、 call 和 bind 调用模式,而后是方法调用模式,而后 是函数调用模式。
它的功能是把对应的字符串解析成 JS 代码并运行。 应该避免使用 eval,不安全,很是耗性能(2次,一次解析成 js 语句,一次执行)。
详细资料能够参考:
《eval()》
DOM 指的是文档对象模型,它指的是把文档当作一个对象来对待,这个对象主要定义了处理网页内容的方法和接口。 BOM 指的是浏览器对象模型,它指的是把浏览器当作一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。BOM 的核心是 window,而 window 对象具备双重角色,它既是经过 js 访问浏览器窗口的一个接口,又是一个 Global(全局) 对象。这意味着在网页中定义的任何对象,变量和函数,都做为全局对象的一个属性或者方法存在。window 对象含有 locati on 对象、navigator 对象、screen 对象等子对象,而且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对 象的子对象。
详细资料能够参考:
《DOM, DOCUMENT, BOM, WINDOW 有什么区别?》
《Window 对象》
《DOM 与 BOM 分别是什么,有何关联?》
《JavaScript 学习总结(三)BOM 和 DOM 详解》
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; } } };
详细资料能够参考:
《JS 事件模型》
详细资料能够参考:
《Javascript 事件模型系列(一)事件及事件的三种模型》
《Javascript 事件模型:事件捕获和事件冒泡》
事件是用户操做网页时发生的交互动做或者网页自己的一些操做,现代浏览器一共有三种事件模型。 第一种事件模型是最先的 DOM0 级模型,这种模型不会传播,因此没有事件流的概念,可是如今有的浏览器支持以冒泡的方式实 现,它能够在网页中直接定义监听函数,也能够经过 js 属性来指定监听函数。这种方式是全部浏览器都兼容的。 第二种事件模型是 IE 事件模型,在该事件模型中,一次事件共有两个过程,事件处理阶段,和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。而后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查通过的节点是否绑定了事件监听函数,若是有则执行。这种模型经过 attachEvent 来添加监听函数,能够添加多个监听函数,会按顺序依次执行。 第三种是 DOM2 级事件模型,在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查通过的节点是否绑定了事件监听函数,若是有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数能够指定事件是否在捕获阶段执行。
详细资料能够参考:
《一个 DOM 元素绑定多个事件时,先执行冒泡仍是捕获》
事件委托本质上是利用了浏览器事件冒泡的机制。由于事件在冒泡过程当中会上传到父节点,而且父节点能够经过事件对象获取到 目标节点,所以能够把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理。 使用事件代理咱们能够没必要要为每个子元素都绑定一个监听事件,这样减小了内存上的消耗。而且使用事件代理咱们还能够实现事件的动态绑定,好比说新增了一个子节点,咱们并不须要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。
详细资料能够参考:
《JavaScript 事件委托详解》
parseInt() 函数能解析一个字符串,并返回一个整数,须要两个参数 (val, radix),其中 radix 表示要解析的数字的基数。(该值介于 2 ~ 36 之间,而且字符串中的数字不能大于 radix 才能正确返回数字结果值)。 此处 map 传了 3 个参数 (element, index, array),默认第三个参数被忽略掉,所以三次传入的参数分别为 "1-0", "2-1", "3-2" 由于字符串的值不能大于基数,所以后面两次调用均失败,返回 NaN ,第一次基数为 0 ,按十进制解析返回 1。
详细资料能够参考:
[《为何 ["1", "2", "3"].map(parseInt) 返回 [1,NaN,NaN]?》](https://blog.csdn.net/justjav...
闭包是指有权访问另外一个函数做用域中变量的函数,建立闭包的最多见的方式就是在一个函数内建立另外一个函数,建立的函数能够 访问到当前函数的局部变量。 闭包有两个经常使用的用途。 闭包的第一个用途是使咱们在函数外部可以访问到函数内部的变量。经过使用闭包,咱们能够经过在外部调用闭包函数,从而在外 部访问到函数内部的变量,能够使用这种方法来建立私有变量。 函数的另外一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,由于闭包函数保留了这个变量对象的引用,因此 这个变量对象不会被回收。 其实闭包的本质就是做用域链的一个特殊的应用,只要了解了做用域链的建立过程,就可以理解闭包的实现原理。
详细资料能够参考:
《JavaScript 深刻理解之闭包》
相关知识点:
use strict 是一种 ECMAscript5 添加的(严格)运行模式,这种模式使得 Javascript 在更严格的条件下运行。 设立"严格模式"的目的,主要有如下几个:
区别:
回答:
use strict 指的是严格运行模式,在这种模式对 js 的使用添加了一些限制。好比说禁止 this 指向全局对象,还有禁止使 用 with 语句等。设立严格模式的目的,主要是为了消除代码使用中的一些不安全的使用方式,也是为了消除 js 语法自己的一 些不合理的地方,以此来减小一些运行时的怪异的行为。同时使用严格运行模式也可以提升编译的效率,从而提升代码的运行速度。 我认为严格模式表明了 js 一种更合理、更安全、更严谨的发展方向。
详细资料能够参考:
《Javascript 严格模式详解》
第一种方式是使用 instanceof 运算符来判断构造函数的 prototype 属性是否出如今对象的原型链中的任何位置。 第二种方式能够经过对象的 constructor 属性来判断,对象的 constructor 属性指向该对象的构造函数,可是这种方式不是很安全,由于 constructor 属性能够被改写。 第三种方式,若是须要判断的是某个内置的引用类型的话,能够使用 Object.prototype.toString() 方法来打印对象的 [[Class]] 属性来进行判断。
详细资料能够参考:
《js 判断一个对象是否属于某一类》
// 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); } }
详细资料能够参考:
《instanceof》
// (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(构造函数, 初始化参数);
详细资料能够参考:
《new 操做符具体干了什么?》
《JavaScript 深刻之 new 的模拟实现》
hasOwnProperty 全部继承了 Object 的对象都会继承到 hasOwnProperty 方法。这个方法能够用来检测一个对象是否含有特定的自身属性,和 in 运算符不一样,该方法会忽略掉那些从原型链上继承到的属性。
详细资料能够参考:
《Object.prototype.hasOwnProperty()》
相关知识点:
JSON 是一种数据交换格式,基于文本,优于轻量,用于交换数据。 JSON 能够表示数字、布尔值、字符串、null、数组(值的有序序列),以及由这些值(或数组、对象)所组成的对象(字符串与 值的映射)。 JSON 使用 JavaScript 语法,可是 JSON 格式仅仅是一个文本。文本能够被任何编程语言读取及做为数据格式传递。
回答:
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 数据结构,以此来进行数据的访问。
详细资料能够参考:
《深刻了解 JavaScript 中的 JSON 》
(1)选取页面全部 DOM 元素。在浏览器的控制台中能够使用$$()方法来获取页面中相应的元素,这是现代浏览器提供的一个命令行 API 至关于 document.querySelectorAll 方法。 (2)循环遍历 DOM 元素 (3)给元素添加 outline 。因为渲染的 outline 是不在 CSS 盒模型中的,因此为元素添加 outline 并不会影响元素的大小和页面的布局。 (4)生成随机颜色函数。Math.random()*(1<<24) 能够获得 0~2^24 - 1 之间的随机数,由于获得的是一个浮点数,但咱们只须要整数部分,使用取反操做符 ~ 连续两次取反得到整数部分,而后再用 toString(16) 的方式,转换为一个十六进制的字符串。
详细资料能够参考:
《经过一行代码学 JavaScript》
相关知识点:
js 延迟加载,也就是等页面加载完成以后再加载 JavaScript 文件。 js 延迟加载有助于提升页面加载速度。
通常有如下几种方式:
回答:
js 的加载、解析和执行会阻塞页面的渲染过程,所以咱们但愿 js 脚本可以尽量的延迟加载,提升页面的渲染速度。 我了解到的几种方式是: 第一种方式是咱们通常采用的是将 js 脚本放在文档的底部,来使 js 脚本尽量的在最后来加载执行。 第二种方式是给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,而后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来讲最后是顺序执行的,可是在一些浏览器中可能不是这样。 第三种方式是给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,可是当脚本加载完成后当即执行 js 脚本,这个时候若是文档没有解析完成的话一样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,通常不会按照代码的顺序依次执行。 第四种方式是动态建立 DOM 标签的方式,咱们能够对文档的加载事件进行监听,当文档加载完成后再动态的建立 script 标签来引入 js 脚本。
详细资料能够参考:
《JS 延迟加载的几种方式》
《HTML 5 <script>
async
属性》
相关知识点:
2005 年 2 月,AJAX 这个词第一次正式提出,它是 Asynchronous JavaScript and XML 的缩写,指的是经过 JavaScript 的
异步通讯,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
具体来讲,AJAX 包括如下几个步骤。
通常实现:
const SERVER_URL = "/server"; let xhr = new XMLHttpRequest(); // 建立 Http 请求 xhr.open("GET", SERVER_URL, true); // 设置状态监听函数 xhr.onreadystatechange = function() { if (this.readyState !== 4) return; // 当请求成功时 if (this.status === 200) { handle(this.response); } else { console.error(this.statusText); } }; // 设置请求失败时的监听函数 xhr.onerror = function() { console.error(this.statusText); }; // 设置请求头信息 xhr.responseType = "json"; xhr.setRequestHeader("Accept", "application/json"); // 发送 Http 请求 xhr.send(null); // promise 封装实现: function getJSON(url) { // 建立一个 promise 对象 let promise = new Promise(function(resolve, reject) { let xhr = new XMLHttpRequest(); // 新建一个 http 请求 xhr.open("GET", url, true); // 设置状态的监听函数 xhr.onreadystatechange = function() { if (this.readyState !== 4) return; // 当请求成功或失败时,改变 promise 的状态 if (this.status === 200) { resolve(this.response); } else { reject(new Error(this.statusText)); } }; // 设置错误监听函数 xhr.onerror = function() { reject(new Error(this.statusText)); }; // 设置响应的数据类型 xhr.responseType = "json"; // 设置请求头信息 xhr.setRequestHeader("Accept", "application/json"); // 发送 http 请求 xhr.send(null); }); return promise; }
回答:
我对 ajax 的理解是,它是一种异步通讯的方法,经过直接由 js 脚本向服务器发起 http 通讯,而后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。 建立一个 ajax 有这样几个步骤 首先是建立一个 XMLHttpRequest 对象。 而后在这个对象上使用 open 方法建立一个 http 请求,open 方法所须要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。 在发起请求前,咱们能够为这个对象添加一些信息和监听函数。好比说咱们能够经过 setRequestHeader 方法来为请求添加头信息。咱们还能够为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,咱们能够经过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,表明服务器返回的数据接收完成,这个时候咱们能够经过判断请求的状态,若是状态是 2xx 或者 304 的话则表明返回正常。这个时候咱们就能够经过 response 中的数据来对页面进行更新了。 当对象的属性和监听函数设置完成后,最后咱们调用 sent 方法来向服务器发起请求,能够传入参数做为发送的数据体。
详细资料能够参考:
《XMLHttpRequest 对象》
《从 ajax 到 fetch、axios》
《Fetch 入门》
《传统 Ajax 已死,Fetch 永生》
浏览器的缓存机制指的是经过在一段时间内保留已接收到的 web 资源的一个副本,若是在资源的有效时间内,发起了对这个资源的再一次请求,那么浏览器会直接使用缓存的副本,而不是向服务器发起请求。使用 web 缓存能够有效地提升页面的打开速度,减小没必要要的网络带宽的消耗。 web 资源的缓存策略通常由服务器来指定,能够分为两种,分别是强缓存策略和协商缓存策略。 使用强缓存策略时,若是缓存资源有效,则直接使用缓存资源,没必要再向服务器发起请求。强缓存策略能够经过两种方式来设置,分别是 http 头信息中的 Expires 属性和 Cache-Control 属性。 服务器经过在响应头中添加 Expires 属性,来指定资源的过时时间。在过时时间之内,该资源能够被缓存使用,没必要再向服务器发送请求。这个时间是一个绝对时间,它是服务器的时间,所以可能存在这样的问题,就是客户端的时间和服务器端的时间不一致,或者用户能够对客户端时间进行修改的状况,这样就可能会影响缓存命中的结果。 Expires 是 http1.0 中的方式,由于它的一些缺点,在 http 1.1 中提出了一个新的头部属性就是 Cache-Control 属性, 它提供了对资源的缓存的更精确的控制。它有不少不一样的值,经常使用的好比咱们能够经过设置 max-age 来指定资源可以被缓存的时间 的大小,这是一个相对的时间,它会根据这个时间的大小和资源第一次请求时的时间来计算出资源过时的时间,所以相对于 Expires 来讲,这种方式更加有效一些。经常使用的还有好比 private ,用来规定资源只能被客户端缓存,不可以代理服务器所缓存。还有如 n o-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 属性。 强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一块儿合做使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,若是命中则直接使用资源。若是不命中则根据头信息向服务器发起请求,使用协商缓存,若是协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,若是协商缓存不命中,则浏览器返回最新的资源给浏览器。
详细资料能够参考:
《浅谈浏览器缓存》
《前端优化:浏览器缓存技术介绍》
《请求头中的 Cache-Control》
《Cache-Control 字段值详解》
详细资料能够参考:
《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)使用服务器来代理跨域的访问请求,就是有跨域的请求操做时发送请求给后端,让后端代为请求,而后最后将获取的结果发返回。
详细资料能够参考:
《前端常见跨域解决方案(全)》
《浏览器同源政策及其规避方法》
《跨域,你须要知道的全在这里》
《为何 form 表单提交没有跨域问题,但 ajax 提交有跨域问题?》
详细资料能够参考:
《深刻浅出 Nginx》
个人理解是 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,也不会被自动添加到请求头部,除非显示地规定。
详细资料能够参考:
《HTTP cookies》
《聊一聊 cookie》
我对模块的理解是,一个模块是实现一个特定功能的一组方法。在最开始的时候,js 只实现一些简单的功能,因此并无模块的概念 ,但随着程序愈来愈复杂,代码的模块化开发变得愈来愈重要。 因为函数具备独立做用域的特色,最原始的写法是使用函数来做为模块,几个函数做为一个模块,可是这种方式容易形成全局变量的污 染,而且模块间没有联系。 后面提出了对象写法,经过将函数做为一个对象的方法来实现,这样解决了直接使用函数做为模块的一些缺点,可是这种办法会暴露所 有的全部的模块成员,外部代码能够修改内部属性的值。 如今最经常使用的是当即执行函数的写法,经过利用闭包来实现模块私有做用域的创建,同时不会对全局做用域形成污染。
详细资料能够参考:
《浅谈模块化开发》
《Javascript 模块化编程(一):模块的写法》
《前端模块化:CommonJS,AMD,CMD,ES6》
《Module 的语法》
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(); // ... });
详细资料能够参考:
《前端模块化,AMD 与 CMD 的区别》
require.js 的核心原理是经过动态建立 script 脚原本异步引入模块,而后对每一个脚本的 load 事件进行监听,若是每一个脚本都加载完成了,再调用回调函数。
详细资料能够参考:
《requireJS 的用法和原理分析》
《requireJS 的核心原理是什么?》
《从 RequireJs 源码剖析脚本加载原理》
《requireJS 原理分析》
详细资料能够参考:
《JS 模块加载器加载原理是怎么样的?》
在我看来 ES6 新添加的 class 只是为了补充 js 中缺乏的一些面向对象语言的特性,但本质上来讲它只是一种语法糖,不是一个新的东西,其背后仍是原型继承的思想。经过加入 class 能够有利于咱们更好的组织代码。 在 class 中添加的方法,实际上是添加在类的原型上的。
详细资料能够参考:
《ECMAScript 6 实现了 class,对 JavaScript 前端开发有什么意义?》
《Class 的基本语法》
document.write 的内容会代替整个文档内容,会重写整个页面。 innerHTML 的内容只是替代指定元素的内容,只会重写页面中的部份内容。
详细资料能够参考:
《简述 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);
详细资料能够参考:
《DOM 概述》
《原生 JavaScript 的 DOM 操做汇总》
《原生 JS 中 DOM 节点相关 API 合集》
对于这样一个 HTML 元素:<div>content<br/></div>。 innerHTML:内部 HTML,content<br/>; outerHTML:外部 HTML,<div>content<br/></div>; innerText:内部文本,content ; outerText:内部文本,content ;
它们的做用如出一辙,区别仅在于传入参数的形式的不一样。 apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合能够为数组,也能够为类数组,apply 方法把这个集合中的元素做为参数传递给被调用的函数。 call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是表明函数体内的 this 指向,从第二个参数开始日后,每一个参数被依次传入函数。
详细资料能够参考:
《apply、call 的区别和用途》
一个拥有 length 属性和若干索引属性的对象就能够被称为类数组对象,类数组对象和数组相似,可是不能调用数组的方法。 常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也能够被看做是类数组对象,由于它含有 length 属性值,表明可接收的参数个数。
常见的类数组转换为数组的方法有这样几种:
(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);
详细的资料能够参考:
《JavaScript 深刻之类数组对象与 arguments》
《javascript 类数组》
《深刻理解 JavaScript 类数组》
数组和字符串的转换方法:toString()、toLocalString()、join() 其中 join() 方法能够指定转换为字符串时的分隔符。 数组尾部操做的方法 pop() 和 push(),push 方法能够传入多个参数。 数组首部操做的方法 shift() 和 unshift() 重排序的方法 reverse() 和 sort(),sort() 方法能够传入一个函数来进行比较,传入先后两个值,若是返回值为正数,则交换两个参数的位置。 数组链接的方法 concat() ,返回的是拼接好的数组,不影响原数组。 数组截取办法 slice(),用于截取数组中的一部分返回,不影响原数组。 数组插入方法 splice(),影响原数组查找特定项的索引的方法,indexOf() 和 lastIndexOf() 迭代方法 every()、some()、filter()、map() 和 forEach() 方法 数组归并方法 reduce() 和 reduceRight() 方法
详细资料能够参考:
《JavaScript 深刻理解之 Array 类型详解》
fill() 方法用一个固定值填充一个数组中从起始索引到终止索引内的所有元素。不包括终止索引。 fill 方法接受三个参数 value,start 以及 end,start 和 end 参数是可选的,其默认值分别为 0 和 this 对象的 length 属性值。
详细资料能够参考:
《Array.prototype.fill()》
尾后逗号 (有时叫作“终止逗号”)在向 JavaScript 代码添加元素、参数、属性时十分有用。若是你想要添加新的属性,而且上一行已经使用了尾后逗号,你能够仅仅添加新的一行,而不须要修改上一行。这使得版本控制更加清晰,以及代码维护麻烦更少。 JavaScript 一开始就支持数组字面值中的尾后逗号,随后向对象字面值(ECMAScript 5)中添加了尾后逗号。最近(ECMAS cript 2017),又将其添加到函数参数中。可是 JSON 不支持尾后逗号。 若是使用了多于一个尾后逗号,会产生间隙。 带有间隙的数组叫作稀疏数组(密致数组没有间隙)。稀疏数组的长度为逗号的数 量。
详细资料能够参考:
《尾后逗号》
变量提高的表现是,不管咱们在函数中何处位置声明的变量,好像都被提高到了函数的首部,咱们能够在变量声明前访问到而不会报错。 形成变量声明提高的本质缘由是 js 引擎在代码执行前有一个解析的过程,建立了执行上下文,初始化了一些代码执行时须要用到的对象。当咱们访问一个变量时,咱们会到当前执行上下文中的做用域链中去查找,而做用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、全部的函数和变量声明,这个对象的是在代码解析的时候建立的。这就是会出现变量声明提高的根本缘由。
详细资料能够参考:
《JavaScript 深刻理解之变量对象》
详细资料能够参考:
《如何编写高性能的 Javascript?》
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 引入了增量标记的方法,将一次停顿进行的过程分为了多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行。
详细资料能够参考:
《深刻理解 V8 的垃圾回收原理》
《JavaScript 中的垃圾回收》
相关知识点:
回答:
第一种状况是咱们因为使用未声明的变量,而意外的建立了一个全局变量,而使这个变量一直留在内存中没法被回收。 第二种状况是咱们设置了 setInterval 定时器,而忘记取消它,若是循环函数有对外部变量的引用的话,那么这个变量会被一直留 在内存中,而没法被回收。 第三种状况是咱们获取一个 DOM 元素的引用,然后面这个元素被删除,因为咱们一直保留了对这个元素的引用,因此它也没法被回 收。 第四种状况是不合理的使用闭包,从而致使某些变量一直被留在内存当中。
详细资料能够参考:
《JavaScript 内存泄漏教程》
《4 类 JavaScript 内存泄漏及如何避免》
《杜绝 js 中四种内存泄漏类型的发生》
《javascript 典型内存泄漏及 chrome 的排查方法》
经过使用 pushState + ajax 实现浏览器无刷新前进后退,当一次 ajax 调用成功后咱们将一条 state 记录加入到 history 对象中。一条 state 记录包含了 url、title 和 content 属性,在 popstate 事件中能够获取到这个 state 对象,咱们可 以使用 content 来传递数据。最后咱们经过对 window.onpopstate 事件监听来响应浏览器的前进后退操做。 使用 pushState 来实现有两个问题,一个是打开首页时没有记录,咱们能够使用 replaceState 来将首页的记录替换,另外一个问 题是当一个页面刷新的时候,仍然会向服务器端请求数据,所以若是请求的 url 须要后端的配合将其重定向到一个页面。
详细资料能够参考:
《pushState + ajax 实现浏览器无刷新前进后退》
《Manipulating the browser history》
this === window ? 'browser' : 'node'; 经过判断 Global 对象是否为 window,若是不为 window,当前脚本没有运行在浏览器中。
详细资料能够参考:
《为何把 script 标签放在 body 结束标签以后 html 结束标签以前?》
《从 Chrome 源码看浏览器如何加载资源》
移动端点击有 300ms 的延迟是由于移动端会有双击缩放的这个操做,所以浏览器在 click 以后要等待 300ms,看用户有没有下一次点击,来判断此次操做是否是双击。
有三种办法来解决这个问题:
click 延时问题还可能引发点击穿透的问题,就是若是咱们在一个元素上注册了 touchStart 的监听事件,这个事件会将这个元素隐藏掉,咱们发现当这个元素隐藏后,触发了这个元素下的一个元素的点击事件,这就是点击穿透。
详细资料能够参考:
《移动端 300ms 点击延迟和点击穿透》
(1)什么是前端路由? 前端路由就是把不一样路由对应不一样的内容或页面的任务交给前端来作,以前是经过服务端根据 url 的不一样返回不一样的页面实现的。 (2)何时使用前端路由? 在单页面应用,大部分页面结构不变,只改变部份内容的使用 (3)前端路由有什么优势和缺点? 优势:用户体验好,不须要每次都从服务器所有获取,快速展示给用户 缺点:单页面没法记住以前滚动的位置,没法在前进,后退的时候记住滚动的位置 前端路由一共有两种实现方式,一种是经过 hash 的方式,一种是经过使用 pushState 的方式。
详细资料能够参考:
《什么是“前端路由”》
《浅谈前端路由》
《前端路由是什么东西?》
详细资料能够参考:
《浅谈前端单元测试》
检测浏览器版本一共有两种方式: 一种是检测 window.navigator.userAgent 的值,但这种方式很不可靠,由于 userAgent 能够被改写,而且早期的浏览器如 ie,会经过假装本身的 userAgent 的值为 Mozilla 来躲过服务器的检测。 第二种方式是功能检测,根据每一个浏览器独有的特性来进行判断,如 ie 下独有的 ActiveXObject。
详细资料能够参考:
《JavaScript 判断浏览器类型》
Polyfill 指的是用于实现浏览器并不支持的原生 API 的代码。 好比说 querySelectorAll 是不少现代浏览器都支持的原生 Web API,可是有些古老的浏览器并不支持,那么假设有人写了一段代码来实现这个功能使这些浏览器也支持了这个功能,那么这就能够成为一个 Polyfill。 一个 shim 是一个库,有本身的 API,而不是单纯实现原生不支持的 API。
详细资料能够参考:
《Web 开发中的“黑话”》
《Polyfill 为什么物》
// 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 秒内事件又被触发,则从新计时。 // 函数节流: 规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,若是在同一个单位时间内某事件被触发屡次,只有一次能生效。 // 函数防抖的实现 function debounce(fn, wait) { var timer = null; return function() { var context = this, args = arguments; // 若是此时存在定时器的话,则取消以前的定时器从新记时 if (timer) { clearTimeout(timer); timer = null; } // 设置定时器,使事件间隔指定事件后执行 timer = setTimeout(() => { fn.apply(context, args); }, wait); }; } // 函数节流的实现; function throttle(fn, delay) { var preTime = Date.now(); return function() { var context = this, args = arguments, nowTime = Date.now(); // 若是两次时间间隔超过了指定时间,则执行函数。 if (nowTime - preTime >= delay) { preTime = Date.now(); return fn.apply(context, args); } }; }
回答:
函数防抖是指在事件被触发 n 秒后再执行回调,若是在这 n 秒内事件又被触发,则从新计时。这能够使用在一些点击请求的事件上,避免由于用户的屡次点击向后端发送屡次请求。 函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,若是在同一个单位时间内某事件被触发屡次,只有一次能生效。节流能够使用在 scroll 函数的事件监听上,经过事件节流来下降事件调用的频率。
详细资料能够参考:
《轻松理解 JS 函数节流和函数防抖》
《JavaScript 事件节流和事件防抖》
《JS 的防抖与节流》
相关知识点:
两等号判等,会在比较时进行类型转换。 三等号判等(判断严格),比较时不进行隐式类型转换,(类型不一样则会返回false)。 Object.is 在三等号判等的基础上特别处理了 NaN 、-0 和 +0 ,保证 -0 和 +0 再也不相同,但 Object.is(NaN, NaN) 会返回 true. Object.is 应被认为有其特殊的用途,而不能用它认为它比其它的相等对比更宽松或严格。
回答:
使用双等号进行相等判断时,若是两边的类型不一致,则会进行强制类型转化后再进行比较。 使用三等号进行相等判断时,若是两边的类型不一致时,不会作强制类型准换,直接返回 false。 使用 Object.is 来进行相等判断时,通常状况下和三等号的判断相同,它处理了一些特殊的状况,好比 -0 和 +0 再也不相等,两个 NaN 认定为是相等的。
相关知识点:
escape 和 encodeURI 都属于 Percent-encoding,基本功能都是把 URI 非法字符转化成合法字符,转化后形式相似「%*」。 它们的根本区别在于,escape 在处理 0xff 以外字符的时候,是直接使用字符的 unicode 在前面加上一个「%u」,而 encode URI 则是先进行 UTF-8,再在 UTF-8 的每一个字节码前加上一个「%」;在处理 0xff 之内字符时,编码方式是同样的(都是「%XX」,XX 为字符的 16 进制 unicode,同时也是字符的 UTF-8),只是范围(即哪些字符编码哪些字符不编码)不同。
回答:
encodeURI 是对整个 URI 进行转义,将 URI 中的非法字符转换为合法字符,因此对于一些在 URI 中有特殊意义的字符不会进行转义。 encodeURIComponent 是对 URI 的组成部分进行转义,因此一些特殊字符也会获得转义。 escape 和 encodeURI 的做用相同,不过它们对于 unicode 编码为 0xff 以外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式,再在每一个字节前加上 %。
详细资料能够参考:
《escape,encodeURI,encodeURIComponent 有什么区别?》
Unicode 是一种字符集合,如今可容纳 100 多万个字符。每一个字符对应一个不一样的 Unicode 编码,它只规定了符号的二进制代码,却没有规定这个二进制代码在计算机中如何编码传输。 UTF-8 是一种对 Unicode 的编码方式,它是一种变长的编码方式,能够用 1~4 个字节来表示一个字符。
详细资料能够参考:
《字符编码详解》
《字符编码笔记:ASCII,Unicode 和 UTF-8》
相关知识点:
事件队列是一个存储着待执行任务的队列,其中的任务严格按照时间前后顺序执行,排在队头的任务将会率先执行,而排在队尾的任务会最后执行。事件队列每次仅执行一个任务,在该任务执行完毕以后,再执行下一个任务。执行栈则是一个相似于函数调用栈的运行容器,当执行栈为空时,JS 引擎便检查事件队列,若是不为空的话,事件队列便将第一个任务压入执行栈中运行。
回答:
由于 js 是单线程运行的,在代码执行的时候,经过将不一样函数的执行上下文压入执行栈中来保证代码的有序执行。在执行同步代码的时候,若是遇到了异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其余任务。当异步事件执行完毕后,再将异步事件对应的回调加入到与当前执行栈中不一样的另外一个任务队列中等待执行。任务队列能够分为宏任务对列和微任务对列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务对列中是否有任务能够执行,若是有就将微任务队首的事件压入栈中执行。当微任务对列中的任务都执行完成后再去判断宏任务对列中的任务。 微任务包括了 promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。 宏任务包括了 script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操做、UI 渲 染等。
详细资料能够参考:
《浏览器事件循环机制(event loop)》
《详解 JavaScript 中的 Event Loop(事件循环)机制》
《什么是 Event Loop?》
《这一次,完全弄懂 JavaScript 执行机制》
相关资料:
// 浅拷贝的实现; function shallowCopy(object) { // 只拷贝对象 if (!object || typeof object !== "object") return; // 根据 object 的类型判断是新建一个数组仍是对象 let newObject = Array.isArray(object) ? [] : {}; // 遍历 object,而且判断是 object 的属性才拷贝 for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = object[key]; } } return newObject; } // 深拷贝的实现; function deepCopy(object) { if (!object || typeof object !== "object") return; let newObject = Array.isArray(object) ? [] : {}; for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = typeof object[key] === "object" ? deepCopy(object[key]) : object[key]; } } return newObject; }
回答:
浅拷贝指的是将一个对象的属性值复制到另外一个对象,若是有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,所以两个对象会有同一个引用类型的引用。浅拷贝能够使用 Object.assign 和展开运算符来实现。 深拷贝相对浅拷贝而言,若是遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,所以对象得到的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象能够使用 JSON 的两个函数来实现,可是因为 JSON 的对象格式比 js 的对象格式更加严格,因此若是属性值里边出现函数或者 Symbol 类型的值时,会转换失败。
详细资料能够参考:
《JavaScript 专题之深浅拷贝》
《前端面试之道》
相关资料:
// call函数实现 Function.prototype.myCall = function(context) { // 判断调用对象 if (typeof this !== "function") { console.error("type error"); } // 获取参数 let args = [...arguments].slice(1), result = null; // 判断 context 是否传入,若是未传入则设置为 window context = context || window; // 将调用函数设为对象的方法 context.fn = this; // 调用函数 result = context.fn(...args); // 将属性删除 delete context.fn; return result; }; // apply 函数实现 Function.prototype.myApply = function(context) { // 判断调用对象是否为函数 if (typeof this !== "function") { throw new TypeError("Error"); } let result = null; // 判断 context 是否存在,若是未传入则为 window context = context || window; // 将函数设为对象的方法 context.fn = this; // 调用方法 if (arguments[1]) { result = context.fn(...arguments[1]); } else { result = context.fn(); } // 将属性删除 delete context.fn; return result; }; // bind 函数实现 Function.prototype.myBind = function(context) { // 判断调用对象是否为函数 if (typeof this !== "function") { throw new TypeError("Error"); } // 获取参数 var args = [...arguments].slice(1), fn = this; return function Fn() { // 根据调用方式,传入不一样绑定值 return fn.apply( this instanceof Fn ? this : context, args.concat(...arguments) ); }; };
回答:
call 函数的实现步骤:
apply 函数的实现步骤:
bind 函数的实现步骤:
详细资料能够参考:
《手写 call、apply 及 bind 函数》
《JavaScript 深刻之 call 和 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); }
详细资料能够参考:
《JavaScript 专题之函数柯里化》
当计算机计算 0.1+0.2 的时候,实际上计算的是这两个数字在计算机里所存储的二进制,0.1 和 0.2 在转换为二进制表示的时候会出现位数无限循环的状况。js 中是以 64 位双精度格式来存储数字的,只有 53 位的有效数字,超过这个长度的位数会被截取掉这样就形成了精度丢失的问题。这是第一个会形成精度丢失的地方。在对两个以 64 位双精度格式的数据进行计算的时候,首先会进行对阶的处理,对阶指的是将阶码对齐,也就是将小数点的位置对齐后,再进行计算,通常是小阶向大阶对齐,所以小阶的数在对齐的过程当中,有效数字会向右移动,移动后超过有效位数的位会被截取掉,这是第二个可能会出现精度丢失的地方。当两个数据阶码对齐后,进行相加运算后,获得的结果可能会超过 53 位有效数字,所以超过的位数也会被截取掉,这是可能发生精度丢失的第三个地方。 对于这样的状况,咱们能够将其转换为整数后再进行运算,运算后再转换为对应的小数,以这种方式来解决这个问题。 咱们还能够将两个数相加的结果和右边相减,若是相减的结果小于一个极小数,那么咱们就能够认定结果是相等的,这个极小数能够 使用 es6 的 Number.EPSILON
详细资料能够参考:
《十进制的 0.1 为何不能用二进制很好的表示?》
《十进制浮点数转成二进制》
《浮点数的二进制表示》
《js 浮点数存储精度丢失原理》
《浮点数精度之谜》
《JavaScript 浮点数陷阱及解法》
《0.1+0.2 !== 0.3?》
《JavaScript 中奇特的~运算符》
原码是计算机中对数字的二进制的定点表示方法,最高位表示符号位,其他位表示数值位。优势是易于分辨,缺点是不可以直接参与运算。 正数的反码和其原码同样;负数的反码,符号位为1,数值部分按原码取反。 如 [+7]原 = 00000111,[+7]反 = 00000111; [-7]原 = 10000111,[-7]反 = 11111000。 正数的补码和其原码同样;负数的补码为其反码加1。 例如 [+7]原 = 00000111,[+7]反 = 00000111,[+7]补 = 00000111; [-7]原 = 10000111,[-7]反 = 11111000,[-7]补 = 11111001 之因此在计算机中使用补码来表示负数的缘由是,这样能够将加法运算扩展到全部的数值计算上,所以在数字电路中咱们只须要考虑加法器的设计就好了,而不用再为减法设置新的数字电路。
详细资料能够参考:
《关于 2 的补码》
toPrecision 用于处理精度,精度是从左至右第一个不为 0 的数开始数起。 toFixed 是对小数点后指定位数取整,从小数点开始数起。 Math.round 是将一个数字四舍五入到一个整数。
XSS 攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者经过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。 XSS 的本质是由于网站没有对恶意代码进行过滤,与正常的代码混合在一块儿了,浏览器没有办法分辨哪些脚本是可信的,从而致使了恶意代码的执行。 XSS 通常分为存储型、反射型和 DOM 型。 存储型指的是恶意代码提交到了网站的数据库中,当用户请求数据的时候,服务器将其拼接为 HTML 后返回给了用户,从而致使了恶意代码的执行。 反射型指的是攻击者构建了特殊的 URL,当服务器接收到请求后,从 URL 中获取数据,拼接到 HTML 后返回,从而致使了恶意代码的执行。 DOM 型指的是攻击者构建了特殊的 URL,用户打开网站后,js 脚本从 URL 中获取数据,从而致使了恶意代码的执行。 XSS 攻击的预防能够从两个方面入手,一个是恶意代码提交的时候,一个是浏览器执行恶意代码的时候。 对于第一个方面,若是咱们对存入数据库的数据都进行的转义处理,可是一个数据可能在多个地方使用,有的地方可能不须要转义,因为咱们没有办法判断数据最后的使用场景,因此直接在输入端进行恶意代码的处理,实际上是不太可靠的。 所以咱们能够从浏览器的执行来进行预防,一种是使用纯前端的方式,不用服务器端拼接后返回。另外一种是对须要插入到 HTML 中的代码作好充分的转义。对于 DOM 型的攻击,主要是前端脚本的不可靠而形成的,咱们对于数据获取渲染和字符串拼接的时候应该对可能出现的恶意代码状况进行判断。 还有一些方式,好比使用 CSP ,CSP 的本质是创建一个白名单,告诉浏览器哪些外部资源能够加载和执行,从而防止恶意代码的注入攻击。 还能够对一些敏感信息进行保护,好比 cookie 使用 http-only ,使得脚本没法获取。也能够使用验证码,避免脚本假装成用户执行一些操做。
详细资料能够参考:
《前端安全系列(一):如何防止 XSS 攻击?》
CSP 指的是内容安全策略,它的本质是创建一个白名单,告诉浏览器哪些外部资源能够加载和执行。咱们只须要配置规则,如何拦截由浏览器本身来实现。 一般有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式 <meta http-equiv="Content-Security-Policy">
详细资料能够参考:
《内容安全策略(CSP)》
《前端面试之道》
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 请求,且会发生页面跳转的请求所使用。
详细资料能够参考:
《前端安全系列之二:如何防止 CSRF 攻击?》
[《[ HTTP 趣谈] origin, referer 和 host 区别》](https://www.jianshu.com/p/1f9...
Samesite Cookie 表示同站 cookie,避免 cookie 被第三方所利用。 将 Samesite 设为 strict ,这种称为严格模式,表示这个 cookie 在任何状况下都不可能做为第三方 cookie。 将 Samesite 设为 Lax ,这种模式称为宽松模式,若是这个请求是个 GET 请求,而且这个请求改变了当前页面或者打开了新的页面,那么这个 cookie 能够做为第三方 cookie,其他状况下都不能做为第三方 cookie。 使用这种方法的缺点是,由于它不支持子域,因此子域没有办法与主域共享登陆信息,每次转入子域的网站,都回从新登陆。还有一个问题就是它的兼容性不够好。
点击劫持是一种视觉欺骗的攻击手段,攻击者将须要攻击的网站经过 iframe 嵌套的方式嵌入本身的网页中,并将 iframe 设置为透明,在页面中透出一个按钮诱导用户点击。 咱们能够在 http 相应头中设置 X-FRAME-OPTIONS 来防护用 iframe 嵌套的点击劫持攻击。经过不一样的值,能够规定页面在特 定的一些状况才能做为 iframe 来使用。
详细资料能够参考:
《web 安全之--点击劫持攻击与防护技术简介》
SQL 注入攻击指的是攻击者在 HTTP 请求中注入恶意的 SQL 代码,服务器使用参数构建数据库 SQL 命令时,恶意 SQL 被一块儿构 造,破坏原有 SQL 结构,并在数据库中执行,达到编写程序时意料以外结果的攻击行为。
详细资料能够参考:
《Web 安全漏洞之 SQL 注入》
《如何防范常见的 Web 攻击》
MVC、MVP 和 MVVM 是三种常见的软件架构设计模式,主要经过分离关注点的方式来组织代码结构,优化咱们的开发效率。 好比说咱们实验室在之前项目开发的时候,使用单页应用时,每每一个路由页面对应了一个脚本文件,全部的页面逻辑都在一个脚本文件里。页面的渲染、数据的获取,对用户事件的响应全部的应用逻辑都混合在一块儿,这样在开发简单项目时,可能看不出什么问题,当时一旦项目变得复杂,那么整个文件就会变得冗长,混乱,这样对咱们的项目开发和后期的项目维护是很是不利的。 MVC 经过分离 Model、View 和 Controller 的方式来组织代码结构。其中 View 负责页面的显示逻辑,Model 负责存储页面的业务数据,以及对相应数据的操做。而且 View 和 Model 应用了观察者模式,当 Model 层发生改变的时候它会通知有关 View 层更新页面。Controller 层是 View 层和 Model 层的纽带,它主要负责用户与应用的响应操做,当用户与页面产生交互的时候,Co ntroller 中的事件触发器就开始工做了,经过调用 Model 层,来完成对 Model 的修改,而后 Model 层再去通知 View 层更新。 MVP 模式与 MVC 惟一不一样的在于 Presenter 和 Controller。在 MVC 模式中咱们使用观察者模式,来实现当 Model 层数据发生变化的时候,通知 View 层的更新。这样 View 层和 Model 层耦合在一块儿,当项目逻辑变得复杂的时候,可能会形成代码的混乱,而且可能会对代码的复用性形成一些问题。MVP 的模式经过使用 Presenter 来实现对 View 层和 Model 层的解耦。MVC 中的 Controller 只知道 Model 的接口,所以它没有办法控制 View 层的更新,MVP 模式中,View 层的接口暴露给了 Presenter 所以咱们能够在 Presenter 中将 Model 的变化和 View 的变化绑定在一块儿,以此来实现 View 和 Model 的同步更新。这样就实现了对 View 和 Model 的解耦,Presenter 还包含了其余的响应逻辑。 MVVM 模式中的 VM,指的是 ViewModel,它和 MVP 的思想实际上是相同的,不过它经过双向的数据绑定,将 View 和 Model 的同步更新给自动化了。当 Model 发生变化的时候,ViewModel 就会自动更新;ViewModel 变化了,View 也会更新。这样就将 Presenter 中的工做给自动化了。我了解过一点双向数据绑定的原理,好比 vue 是经过使用数据劫持和发布订阅者模式来实现的这一功 能。
详细资料能够参考:
《浅析前端开发中的 MVC/MVP/MVVM 模式》
《MVC,MVP 和 MVVM 的图示》
《MVVM》
《一篇文章了解架构模式:MVC/MVP/MVVM》
vue 经过使用双向数据绑定,来实现了 View 和 Model 的同步更新。vue 的双向数据绑定主要是经过使用数据劫持和发布订阅者模式来实现的。 首先咱们经过 Object.defineProperty() 方法来对 Model 数据各个属性添加访问器属性,以此来实现数据的劫持,所以当 Model 中的数据发生变化的时候,咱们能够经过配置的 setter 和 getter 方法来实现对 View 层数据更新的通知。 数据在 html 模板中一共有两种绑定状况,一种是使用 v-model 来对 value 值进行绑定,一种是做为文本绑定,在对模板引擎进行解析的过程当中。 若是遇到元素节点,而且属性值包含 v-model 的话,咱们就从 Model 中去获取 v-model 所对应的属性的值,并赋值给元素的 value 值。而后给这个元素设置一个监听事件,当 View 中元素的数据发生变化的时候触发该事件,通知 Model 中的对应的属性的值进行更新。 若是遇到了绑定的文本节点,咱们使用 Model 中对应的属性的值来替换这个文本。对于文本节点的更新,咱们使用了发布订阅者模式,属性做为一个主题,咱们为这个节点设置一个订阅者对象,将这个订阅者对象加入这个属性主题的订阅者列表中。当 Model 层数据发生改变的时候,Model 做为发布者向主题发出通知,主题收到通知再向它的全部订阅者推送,订阅者收到通知后更改本身的数 据。
详细资料能够参考:
《Vue.js 双向绑定的实现原理》
Object.defineProperty 函数一共有三个参数,第一个参数是须要定义属性的对象,第二个参数是须要定义的属性,第三个是该属性描述符。 一个属性的描述符有四个属性,分别是 value 属性的值,writable 属性是否可写,enumerable 属性是否可枚举,configurable 属性是否可配置修改。
详细资料能够参考:
《Object.defineProperty()》
有一些对属性的操做,使用这种方法没法拦截,好比说经过下标方式修改数组数据或者给对象新增属性,vue 内部经过重写函数解决了这个问题。在 Vue3.0 中已经不使用这种方式了,而是经过使用 Proxy 对对象进行代理,从而实现数据劫持。使用 Proxy 的好处是它能够完美的监听到任何方式的数据改变,惟一的缺点是兼容性的问题,由于这是 ES6 的语法。
我对 Virtual DOM 的理解是, 首先对咱们将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,好比一个元素对象,包含 TagName、props 和 Children 这些属性。而后咱们将这个 js 对象树给保存下来,最后再将 DOM 片断插入到文档中。 当页面的状态发生改变,咱们须要对页面的 DOM 的结构进行调整的时候,咱们首先根据变动的状态,从新构建起一棵对象树,而后将这棵新的对象树和旧的对象树进行比较,记录下两棵树的的差别。 最后将记录的有差别的地方应用到真正的 DOM 树中去,这样视图就更新了。 我认为 Virtual DOM 这种方法对于咱们须要有大量的 DOM 操做的时候,可以很好的提升咱们的操做效率,经过在操做前肯定须要作的最小修改,尽量的减小 DOM 操做带来的重流和重绘的影响。其实 Virtual DOM 并不必定比咱们真实的操做 DOM 要快,这种方法的目的是为了提升咱们开发时的可维护性,在任意的状况下,都能保证一个尽可能小的性能消耗去进行操做。
详细资料能够参考:
《Virtual DOM》
《理解 Virtual DOM》
《深度剖析:如何实现一个 Virtual DOM 算法》
《网上都说操做真实 DOM 慢,但测试结果却比 React 更快,为何?》
两个树的彻底 diff 算法的时间复杂度为 O(n^3) ,可是在前端中,咱们不多会跨层级的移动元素,因此咱们只须要比较同一层级的元素进行比较,这样就能够将算法的时间复杂度下降为 O(n)。 算法首先会对新旧两棵树进行一个深度优先的遍历,这样每一个节点都会有一个序号。在深度遍历的时候,每遍历到一个节点,咱们就将这个节点和新的树中的节点进行比较,若是有差别,则将这个差别记录到一个对象中。 在对列表元素进行对比的时候,因为 TagName 是重复的,因此咱们不能使用这个来对比。咱们须要给每个子节点加上一个 key,列表对比的时候使用 key 来进行比较,这样咱们才可以复用老的 DOM 树上的节点。
详细资料能够参考:
《你须要知道的 requestAnimationFrame》
《CSS3 动画那么强,requestAnimationFrame 还有毛线用?》
我当时使用 webpack 的一个最主要缘由是为了简化页面依赖的管理,而且经过将其打包为一个文件来下降页面加载时请求的资源 数。 我认为 webpack 的主要原理是,它将全部的资源都当作是一个模块,而且把页面逻辑看成一个总体,经过一个给定的入口文件,webpack 从这个文件开始,找到全部的依赖文件,将各个依赖文件模块经过 loader 和 plugins 处理后,而后打包在一块儿,最后输出一个浏览器可识别的 JS 文件。 Webpack 具备四个核心的概念,分别是 Entry(入口)、Output(输出)、loader 和 Plugins(插件)。 Entry 是 webpack 的入口起点,它指示 webpack 应该从哪一个模块开始着手,来做为其构建内部依赖图的开始。 Output 属性告诉 webpack 在哪里输出它所建立的打包文件,也可指定打包文件的名称,默认位置为 ./dist。 loader 能够理解为 webpack 的编译器,它使得 webpack 能够处理一些非 JavaScript 文件。在对 loader 进行配置的时候,test 属性,标志有哪些后缀的文件应该被处理,是一个正则表达式。use 属性,指定 test 类型的文件应该使用哪一个 loader 进行预处理。经常使用的 loader 有 css-loader、style-loader 等。 插件能够用于执行范围更广的任务,包括打包、优化、压缩、搭建服务器等等,要使用一个插件,通常是先使用 npm 包管理器进行安装,而后在配置文件中引入,最后将其实例化后传递给 plugins 数组属性。 使用 webpack 的确可以提供咱们对于项目的管理,可是它的缺点就是调试和配置起来太麻烦了。但如今 webpack4.0 的免配置必定程度上解决了这个问题。可是我感受就是对我来讲,就是一个黑盒,不少时候出现了问题,没有办法很好的定位。
详细资料能够参考:
《不聊 webpack 配置,来讲说它的原理》
《前端工程化——构建工具选型:grunt、gulp、webpack》
《浅入浅出 webpack》
《前端构建工具发展及其比较》
clientWidth/clientHeight 返回的是元素的内部宽度,它的值只包含 content + padding,若是有滚动条,不包含滚动条。 clientTop 返回的是上边框的宽度。 clientLeft 返回的左边框的宽度。 offsetWidth/offsetHeight 返回的是元素的布局宽度,它的值包含 content + padding + border 包含了滚动条。 offsetTop 返回的是当前元素相对于其 offsetParent 元素的顶部的距离。 offsetLeft 返回的是当前元素相对于其 offsetParent 元素的左部的距离。 scrollWidth/scrollHeight 返回值包含 content + padding + 溢出内容的尺寸。 scrollTop 属性返回的是一个元素的内容垂直滚动的像素数。 scrollLeft 属性返回的是元素滚动条到元素左边的距离。
详细资料能够参考:
《最全的获取元素宽高及位置的方法》
《用 Javascript 获取页面元素的位置》
简单说,"函数式编程"是一种"编程范式"(programming paradigm),也就是如何编写程序的方法论。 它具备如下特性:闭包和高阶函数、惰性计算、递归、函数是"第一等公民"、只用"表达式"。
详细资料能够参考:
《函数式编程初探》
相关资料:
回调函数 优势:简单、容易理解 缺点:不利于维护,代码耦合高 事件监听(采用时间驱动模式,取决于某个事件是否发生): 优势:容易理解,能够绑定多个事件,每一个事件能够指定多个回调函数 缺点:事件驱动型,流程不够清晰 发布/订阅(观察者模式) 相似于事件监听,可是能够经过‘消息中心’,了解如今有多少发布者,多少订阅者 Promise 对象 优势:能够利用 then 方法,进行链式写法;能够书写错误时的回调函数; 缺点:编写和理解,相对比较难 Generator 函数 优势:函数体内外的数据交换、错误处理机制 缺点:流程管理不方便 async 函数 优势:内置执行器、更好的语义、更广的适用性、返回的是 Promise、结构清晰。 缺点:错误处理机制
回答:
js 中的异步机制能够分为如下几种: 第一种最多见的是使用回调函数的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会形成回调函数地狱,上下两层的回调函数间的代码耦合度过高,不利于代码的可维护。 第二种是 Promise 的方式,使用 Promise 的方式能够将嵌套的回调函数做为链式调用。可是使用这种方法,有时会形成多个 then 的链式调用,可能会形成代码的语义不够明确。 第三种是使用 generator 的方式,它能够在函数的执行过程当中,将函数的执行权转移出去,在函数外部咱们还能够将执行权转移回来。当咱们遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕的时候咱们再将执行权给转移回来。所以咱们在 generator 内部对于异步操做的方式,能够以同步的顺序来书写。使用这种方式咱们须要考虑的问题是什么时候将函数的控制权转移回来,所以咱们须要有一个自动执行 generator 的机制,好比说 co 模块等方式来实现 generator 的自动执行。 第四种是使用 async 函数的形式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,若是语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。所以咱们能够将异步逻辑,转化为同步的顺序来书写,而且这个函数能够自动执行。
CSS3 的动画的优势 在性能上会稍微好一些,浏览器会对 CSS3 的动画作一些优化 代码相对简单 缺点 在动画控制上不够灵活 兼容性很差 JavaScript 的动画正好弥补了这两个缺点,控制能力很强,能够单帧的控制、变换,同时写得好彻底能够兼容 IE6,而且功能强大。对于一些复杂控制的动画,使用 javascript 会比较靠谱。而在实现一些小的交互动效的时候,就多考虑考虑 CSS 吧
误区:咱们常常说 get 请求参数的大小存在限制,而 post 请求的参数大小是无限制的。 实际上 HTTP 协议从未规定 GET/POST 的请求长度限制是多少。对 get 请求参数的限制是来源与浏览器或web 服务器,浏览器或 web 服务器限制了 url 的长度。为了明确这个概念,咱们必须再次强调下面几点:
URI: Uniform Resource Identifier 指的是统一资源标识符 URL: Uniform Resource Location 指的是统一资源定位符 URN: Universal Resource Name 指的是统一资源名称 URI 指的是统一资源标识符,用惟一的标识来肯定一个资源,它是一种抽象的定义,也就是说,无论使用什么方法来定义,只要能惟一的标识一个资源,就能够称为 URI。 URL 指的是统一资源定位符,URN 指的是统一资源名称。URL 和 URN 是 URI 的子集,URL 能够理解为使用地址来标识资源,URN 能够理解为使用名称来标识资源。
详细资料能够参考:
《HTTP 协议中 URI 和 URL 有什么区别?》
《你知道 URL、URI 和 URN 三者之间的区别吗?》
《URI、URL 和 URN 的区别》
相关知识点:
get 请求相似于查找的过程,用户获取数据,能够不用每次都与数据库链接,因此能够使用缓存。 post 不一样,post 作的通常是修改和删除的工做,因此必须与数据库交互,因此不能使用缓存。所以 get 请求适合于请求缓存。
回答:
缓存通常只适用于那些不会更新服务端数据的请求。通常 get 请求都是查找请求,不会对服务器资源数据形成修改,而 post 请求通常都会对服务器数据形成修改,因此,通常会对 get 请求进行缓存,不多会对 post 请求进行缓存。
详细资料能够参考:
《HTML 关于 post 和 get 的区别以及缓存问题的理解》
相关知识点:
预加载:提早加载图片,当用户须要查看时可直接从本地缓存中渲染。 懒加载:懒加载的主要目的是做为服务器前端的优化,减小请求数或延迟请求数。 两种技术的本质:二者的行为是相反的,一个是提早加载,一个是迟缓甚至不加载。 懒加载对服务器前端有必定的缓解压力做用,预加载则会增长服务器前端压力。
回答:
懒加载也叫延迟加载,指的是在长网页中延迟加载图片的时机,当用户须要访问时,再去加载,这样能够提升网站的首屏加载速度,提高用户的体验,而且能够减小服务器的压力。它适用于图片不少,页面很长的电商网站的场景。懒加载的实现原理是,将页面上的图片的 src 属性设置为空字符串,将图片的真实路径保存在一个自定义属性中,当页面滚动的时候,进行判断,若是图片进入页面可视区域内,则从自定义属性中取出真实路径赋值给图片的 src 属性,以此来实现图片的延迟加载。 预加载指的是将所需的资源提早请求加载到本地,这样后面在须要用到时就直接从缓存取资源。经过预加载可以减小用户的等待时间,提升用户的体验。我了解的预加载的最经常使用的方式是使用 js 中的 image 对象,经过为 image 对象来设置 scr 属性,来实现图片的预加载。 这两种方式都是提升网页性能的方式,二者主要区别是一个是提早加载,一个是迟缓甚至不加载。懒加载对服务器前端有必定的缓解压力做用,预加载则会增长服务器前端压力。
详细资料能够参考:
《懒加载和预加载》
《网页图片加载优化方案》
《基于用户行为的图片等资源预加载》
当鼠标移动到元素上时就会触发 mouseenter 事件,相似 mouseover,它们二者之间的差异是 mouseenter 不会冒泡。 因为 mouseenter 不支持事件冒泡,致使在一个元素的子元素上进入或离开的时候会触发其 mouseover 和 mouseout 事件,可是却不会触发 mouseenter 和 mouseleave 事件。
详细资料能够参考:
《mouseenter 与 mouseover 为什么这般纠缠不清?》
相关知识点:
首先是三个事件,分别是 mousedown,mousemove,mouseup 当鼠标点击按下的时候,须要一个 tag 标识此时已经按下,能够执行 mousemove 里面的具体方法。 clientX,clientY 标识的是鼠标的坐标,分别标识横坐标和纵坐标,而且咱们用 offsetX 和 offsetY 来表示 元素的元素的初始坐标,移动的举例应该是: 鼠标移动时候的坐标-鼠标按下去时候的坐标。 也就是说定位信息为: 鼠标移动时候的坐标-鼠标按下去时候的坐标+元素初始状况下的 offetLeft.
回答:
一个元素的拖拽过程,咱们能够分为三个步骤,第一步是鼠标按下目标元素,第二步是鼠标保持按下的状态移动鼠标,第三步是鼠 标抬起,拖拽过程结束。 这三步分别对应了三个事件,mousedown 事件,mousemove 事件和 mouseup 事件。只有在鼠标按下的状态移动鼠标咱们才会 执行拖拽事件,所以咱们须要在 mousedown 事件中设置一个状态来标识鼠标已经按下,而后在 mouseup 事件中再取消这个状 态。在 mousedown 事件中咱们首先应该判断,目标元素是否为拖拽元素,若是是拖拽元素,咱们就设置状态而且保存这个时候鼠 标的位置。而后在 mousemove 事件中,咱们经过判断鼠标如今的位置和之前位置的相对移动,来肯定拖拽元素在移动中的坐标。 最后 mouseup 事件触发后,清除状态,结束拖拽事件。
详细资料能够参考:
《原生 js 实现拖拽功能基本思路》
相关知识点:
// 思路是使用递归函数,不断地去执行 setTimeout 从而达到 setInterval 的效果 function mySetInterval(fn, timeout) { // 控制器,控制定时器是否继续执行 var timer = { flag: true }; // 设置递归函数,模拟定时器执行。 function interval() { if (timer.flag) { fn(); setTimeout(interval, timeout); } } // 启动定时器 setTimeout(interval, timeout); // 返回控制器 return timer; }
回答:
setInterval 的做用是每隔一段指定时间执行一个函数,可是这个执行不是真的到了时间当即执行,它真正的做用是每隔一段时间将事件加入事件队列中去,只有当当前的执行栈为空的时候,才能去从事件队列中取出事件执行。因此可能会出现这样的状况,就是当前执行栈执行的时间很长,致使事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,所以就不能到间隔一段时间执行的效果。 针对 setInterval 的这个缺点,咱们能够使用 setTimeout 递归调用来模拟 setInterval,这样咱们就确保了只有一个事件结束了,咱们才会触发下一个定时器事件,这样解决了 setInterval 的问题。
详细资料能够参考:
《用 setTimeout 实现 setInterval》
《setInterval 有什么缺点?》
rest 参数(形式为...变量名),用于获取函数的多余参数。
尾调用指的是函数的最后一步调用另外一个函数。咱们代码执行是基于执行栈的,因此当咱们在一个函数里调用另外一个函数时,咱们会保留当前的执行上下文,而后再新建另一个执行上下文加入栈中。使用尾调用的话,由于已是函数的最后一步,因此这个时候咱们能够没必要再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。可是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
Proxy 用于修改某些操做的默认行为,等同于在语言层面作出修改,因此属于一种“元编程”,即对编程语言进行编程。 Proxy 能够理解成,在目标对象以前架设一层“拦截”,外界对该对象的访问,都必须先经过这层拦截,所以提供了一种机制,能够对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操做,能够译为“代理器”。
当 Node 遇到 require(X) 时,按下面的顺序处理。 (1)若是 X 是内置模块(好比 require('http')) a. 返回该模块。 b. 再也不继续执行。 (2)若是 X 以 "./" 或者 "/" 或者 "../" 开头 a. 根据 X 所在的父模块,肯定 X 的绝对路径。 b. 将 X 当成文件,依次查找下面文件,只要其中有一个存在,就返回该文件,再也不继续执行。 X X.js X.json X.node c. 将 X 当成目录,依次查找下面文件,只要其中有一个存在,就返回该文件,再也不继续执行。 X/package.json(main字段) X/index.js X/index.json X/index.node (3)若是 X 不带路径 a. 根据 X 所在的父模块,肯定 X 可能的安装目录。 b. 依次在每一个目录中,将 X 当成文件名或目录名加载。 (4)抛出 "not found"
详细资料能够参考:
《require() 源码解读》
Promise 对象是异步编程的一种解决方案,最先由社区提出。Promises/A+ 规范是 JavaScript Promise 的标准,规定了一个 Promise 所必须具备的特性。 Promise 是一个构造函数,接收一个函数做为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是 pending、resolved 和 rejected,分别表明了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者 rejected 状态,而且状态一经改变,就凝固了,没法再被改变了。状态的改变是经过 resolve() 和 reject() 函数来实现的,咱们 能够在异步操做结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法能够为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
详细资料能够参考:
《Promises/A+ 规范》
《Promise》
const PENDING = "pending"; const RESOLVED = "resolved"; const REJECTED = "rejected"; function MyPromise(fn) { // 保存初始化状态 var self = this; // 初始化状态 this.state = PENDING; // 用于保存 resolve 或者 rejected 传入的值 this.value = null; // 用于保存 resolve 的回调函数 this.resolvedCallbacks = []; // 用于保存 reject 的回调函数 this.rejectedCallbacks = []; // 状态转变为 resolved 方法 function resolve(value) { // 判断传入元素是否为 Promise 值,若是是,则状态改变必须等待前一个状态改变后再进行改变 if (value instanceof MyPromise) { return value.then(resolve, reject); } // 保证代码的执行顺序为本轮事件循环的末尾 setTimeout(() => { // 只有状态为 pending 时才能转变, if (self.state === PENDING) { // 修改状态 self.state = RESOLVED; // 设置传入的值 self.value = value; // 执行回调函数 self.resolvedCallbacks.forEach(callback => { callback(value); }); } }, 0); } // 状态转变为 rejected 方法 function reject(value) { // 保证代码的执行顺序为本轮事件循环的末尾 setTimeout(() => { // 只有状态为 pending 时才能转变 if (self.state === PENDING) { // 修改状态 self.state = REJECTED; // 设置传入的值 self.value = value; // 执行回调函数 self.rejectedCallbacks.forEach(callback => { callback(value); }); } }, 0); } // 将两个方法传入函数执行 try { fn(resolve, reject); } catch (e) { // 遇到错误时,捕获错误,执行 reject 函数 reject(e); } } MyPromise.prototype.then = function(onResolved, onRejected) { // 首先判断两个参数是否为函数类型,由于这两个参数是可选参数 onResolved = typeof onResolved === "function" ? onResolved : function(value) { return value; }; onRejected = typeof onRejected === "function" ? onRejected : function(error) { throw error; }; // 若是是等待状态,则将函数加入对应列表中 if (this.state === PENDING) { this.resolvedCallbacks.push(onResolved); this.rejectedCallbacks.push(onRejected); } // 若是状态已经凝固,则直接执行对应状态的函数 if (this.state === RESOLVED) { onResolved(this.value); } if (this.state === REJECTED) { onRejected(this.value); } };
用 JS 设置 DOM 的字体为某一个值,而后再取出来,若是值设置成功,就说明支持。
error 统计使用浏览器的 window.error 事件。
单例模式保证了全局只有一个实例来被访问。好比说经常使用的如弹框组件的实现和全局状态的实现。
策略模式主要是用来将方法的实现和方法的调用分离开,外部经过不一样的参数能够调用不一样的策略。我主要在 MVP 模式解耦的时候 用来将视图层的方法定义和方法调用分离。
代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。好比说常见的事件代理。
中介者模式指的是,多个对象经过一个中介者进行交流,而不是直接进行交流,这样可以将通讯的各个对象解耦。
适配器用来解决两个接口不兼容的状况,不须要改变已有的接口,经过包装一层的方式实现两个接口的正常协做。假如咱们须要一种 新的接口返回方式,可是老的接口因为在太多地方已经使用了,不能随意更改,这个时候就能够使用适配器模式。好比咱们须要一种 自定义的时间返回格式,可是咱们又不能对 js 时间格式化的接口进行修改,这个时候就能够使用适配器模式。
更多关于设计模式的资料能够参考:
《前端面试之道》
《JavaScript 设计模式》
《JavaScript 中常见设计模式整理》
发布订阅模式其实属于广义上的观察者模式 在观察者模式中,观察者须要直接订阅目标事件。在目标发出内容改变的事件后,直接接收事件并做出响应。 而在发布订阅模式中,发布者和订阅者之间多了一个调度中心。调度中心一方面从发布者接收事件,另外一方面向订阅者发布事件,订阅者须要在调度中心中订阅事件。经过调度中心实现了发布者和订阅者关系的解耦。使用发布订阅者模式更利于咱们代码的可维护性。
详细资料能够参考:
《观察者模式和发布订阅模式有什么不一样?》
Vue 的生命周期指的是组件从建立到销毁的一系列的过程,被称为 Vue 的生命周期。经过提供的 Vue 在生命周期各个阶段的钩子函数,咱们能够很好的在 Vue 的各个生命阶段实现一些操做。
Vue 一共有8个生命阶段,分别是建立前、建立后、加载前、加载后、更新前、更新后、销毁前和销毁后,每一个阶段对应了一个生命周期的钩子函数。 (1)beforeCreate 钩子函数,在实例初始化以后,在数据监听和事件配置以前触发。所以在这个事件中咱们是获取不到 data 数据的。 (2)created 钩子函数,在实例建立完成后触发,此时能够访问 data、methods 等属性。但这个时候组件尚未被挂载到页面中去,因此这个时候访问不到 $el 属性。通常咱们能够在这个函数中进行一些页面初始化的工做,好比经过 ajax 请求数据来对页面进行初始化。 (3)beforeMount 钩子函数,在组件被挂载到页面以前触发。在 beforeMount 以前,会找到对应的 template,并编译成 render 函数。 (4)mounted 钩子函数,在组件挂载到页面以后触发。此时能够经过 DOM API 获取到页面中的 DOM 元素。 (5)beforeUpdate 钩子函数,在响应式数据更新时触发,发生在虚拟 DOM 从新渲染和打补丁以前,这个时候咱们能够对可能会被移除的元素作一些操做,好比移除事件监听器。 (6)updated 钩子函数,虚拟 DOM 从新渲染和打补丁以后调用。 (7)beforeDestroy 钩子函数,在实例销毁以前调用。通常在这一步咱们能够销毁定时器、解绑全局事件等。 (8)destroyed 钩子函数,在实例销毁以后调用,调用后,Vue 实例中的全部东西都会解除绑定,全部的事件监听器会被移除,全部的子实例也会被销毁。 当咱们使用 keep-alive 的时候,还有两个钩子函数,分别是 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。
详细资料能够参考:
《vue 生命周期深刻》
《Vue 实例》
(1)父子组件间通讯 第一种方法是子组件经过 props 属性来接受父组件的数据,而后父组件在子组件上注册监听事件,子组件经过 emit 触发事 件来向父组件发送数据。 第二种是经过 ref 属性给子组件设置一个名字。父组件经过 $refs 组件名来得到子组件,子组件经过 $parent 得到父组 件,这样也能够实现通讯。 第三种是使用 provider/inject,在父组件中经过 provider 提供变量,在子组件中经过 inject 来将变量注入到组件 中。不论子组件有多深,只要调用了 inject 那么就能够注入 provider 中的数据。 (2)兄弟组件间通讯 第一种是使用 eventBus 的方法,它的本质是经过建立一个空的 Vue 实例来做为消息传递的对象,通讯的组件引入这个实 例,通讯的组件经过在这个实例上监听和触发事件,来实现消息的传递。 第二种是经过 $parent.$refs 来获取到兄弟组件,也能够进行通讯。 (3)任意组件之间 使用 eventBus ,其实就是建立一个事件中心,至关于中转站,能够用它来传递事件和接收事件。 若是业务逻辑复杂,不少组件之间须要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候 能够使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它做为一个全局的变量来管理,而后其余组件就能够对这个 公共数据进行读写操做,这样达到了解耦的目的。
详细资料能够参考:
《VUE 组件之间数据传递全集》
(1)computed 是计算一个新的属性,并将该属性挂载到 Vue 实例上,而 watch 是监听已经存在且已挂载到 Vue 实例上的数据,因此用 watch 一样能够监听 computed 计算属性的变化。 (2)computed 本质是一个惰性求值的观察者,具备缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值。而 watch 则是当数据发生变化便会调用执行函数。 (3)从使用场景上说,computed 适用一个数据被多个数据影响,而 watch 适用一个数据影响多个数据。
详细资料能够参考:
《作面试的不倒翁:浅谈 Vue 中 computed 实现原理》
《深刻理解 Vue 的 watch 实现原理及其实现方式》
(1)全局的钩子函数 beforeEach 和 afterEach beforeEach 有三个参数,to 表明要进入的路由对象,from 表明离开的路由对象。next 是一个必需要执行的函数,若是不传参数,那就执行下一个钩子函数,若是传入 false,则终止跳转,若是传入一个路径,则导航到对应的路由,若是传入 error ,则导航终止,error 传入错误的监听函数。 (2)单个路由独享的钩子函数 beforeEnter,它是在路由配置上直接进行定义的。 (3)组件内的导航钩子主要有这三种:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。它们是直接在路由组 件内部直接进行定义的。
详细资料能够参考:
《导航守卫》
$route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。而 $router 是“路由实例”对象包括了路由的跳转方法,钩子函数等。
.prevent: 提交事件再也不重载页面;.stop: 阻止单击事件冒泡;.self: 当事件发生在该元素自己而不是子元素的时候会触发;
vue 中 key 值的做用能够分为两种状况来考虑。 第一种状况是 v-if 中使用 key。因为 Vue 会尽量高效地渲染元素,一般会复用已有元素而不是从头开始渲染。所以当咱们使用 v-if 来实现元素切换的时候,若是切换先后含有相同类型的元素,那么这个元素就会被复用。若是是相同的 input 元素,那么切换先后用户的输入不会被清除掉,这样是不符合需求的。所以咱们能够经过使用 key 来惟一的标识一个元素,这个状况下,使用 key 的元素不会被复用。这个时候 key 的做用是用来标识一个独立的元素。 第二种状况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。若是数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每一个元素。所以经过为每一个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的做用是为了高效的更新渲染虚拟 DOM。
详细资料能够参考:
《Vue 面试中,常常会被问到的面试题 Vue 知识点整理》
《Vue2.0 v-for 中 :key 到底有什么用?》
《vue 中 key 的做用》
computed 是计算属性,依赖其余属性计算值,而且 computed 的值有缓存,只有当计算值变化才会返回内容。 watch 监听到值的变化就会执行回调,在回调中能够进行一些逻辑操做。
若是你须要在组件切换的时候,保存一些组件的状态防止屡次渲染,就能够使用 keep-alive 组件包裹须要保存的组件。
mixin 用于全局混入,会影响到每一个组件实例。 mixins 应该是咱们最常使用的扩展组件的方式了。若是多个组件中有相同的业务逻辑,就能够将这些逻辑剥离出来,经过 mixins 混入代码,好比上拉下拉加载数据这种逻辑等等。另外须要注意的是 mixins 混入的钩子函数会先于组件内的钩子函数执行,而且在遇到同名选项的时候也会有选择性的进行合并
(1)application/x-www-form-urlencoded 浏览器的原生 form 表单,若是不设置 enctype 属性,那么最终就会以 application/x-www-form-urlencoded 方式提交数据。该种方式提交的数据放在 body 里面,数据按照 key1=val1&key2=val2 的方式进行编码,key 和 val 都进行了 URL 转码。 (2)multipart/form-data 该种方式也是一个常见的 POST 提交方式,一般表单上传文件时使用该种方式。 (3)application/json 告诉服务器消息主体是序列化后的 JSON 字符串。 (4)text/xml 该种方式主要用来提交 XML 格式的数据。
详细资料能够参考:
《经常使用的几种 Content-Type》
function getType(value) { // 判断数据是 null 的状况 if (value === null) { return value + ""; } // 判断数据是引用类型的状况 if (typeof value === "object") { let valueClass = Object.prototype.toString.call(value), type = valueClass.split(" ")[1].split(""); type.pop(); return type.join("").toLowerCase(); } else { // 判断数据是基本数据类型的状况和函数的状况 return typeof value; } }
详细资料能够参考:
《JavaScript 专题之类型判断(上)》
function checkNullObj(obj) { return Object.keys(obj).length === 0; }
详细资料能够参考:
《js 判断一个 object 对象是否为空》
// 使用闭包实现 for (var i = 0; i < 5; i++) { (function(i) { setTimeout(function() { console.log(i); }, i * 1000); })(i); } // 使用 let 块级做用域 for (let i = 0; i < 5; i++) { setTimeout(function() { console.log(i); }, i * 1000); }
function jsonp(url, params, callback) { // 判断是否含有参数 let queryString = url.indexOf("?") === "-1" ? "?" : "&"; // 添加参数 for (var k in params) { if (params.hasOwnProperty(k)) { queryString += k + "=" + params[k] + "&"; } } // 处理回调函数名 let random = Math.random() .toString() .replace(".", ""), callbackName = "myJsonp" + random; // 添加回调函数 queryString += "callback=" + callbackName; // 构建请求 let scriptNode = document.createElement("script"); scriptNode.src = url + queryString; window[callbackName] = function() { // 调用回调函数 callback(...arguments); // 删除这个引入的脚本 document.getElementsByTagName("head")[0].removeChild(scriptNode); }; // 发起请求 document.getElementsByTagName("head")[0].appendChild(scriptNode); }
详细资料能够参考:
《原生 jsonp 具体实现》
《jsonp 的原理与实现》
var events = (function() { var topics = {}; return { // 注册监听函数 subscribe: function(topic, handler) { if (!topics.hasOwnProperty(topic)) { topics[topic] = []; } topics[topic].push(handler); }, // 发布事件,触发观察者回调事件 publish: function(topic, info) { if (topics.hasOwnProperty(topic)) { topics[topic].forEach(function(handler) { handler(info); }); } }, // 移除主题的一个观察者的回调事件 remove: function(topic, handler) { if (!topics.hasOwnProperty(topic)) return; var handlerIndex = -1; topics[topic].forEach(function(item, index) { if (item === handler) { handlerIndex = index; } }); if (handlerIndex >= 0) { topics[topic].splice(handlerIndex, 1); } }, // 移除主题的全部观察者的回调事件 removeAll: function(topic) { if (topics.hasOwnProperty(topic)) { topics[topic] = []; } } }; })();
详细资料能够参考:
《JS 事件模型》
class EventEmitter { constructor() { this.events = {}; } on(event, callback) { let callbacks = this.events[event] || []; callbacks.push(callback); this.events[event] = callbacks; return this; } off(event, callback) { let callbacks = this.events[event]; this.events[event] = callbacks && callbacks.filter(fn => fn !== callback); return this; } emit(event, ...args) { let callbacks = this.events[event]; callbacks.forEach(fn => { fn(...args); }); return this; } once(event, callback) { let wrapFun = function(...args) { callback(...args); this.off(event, wrapFun); }; this.on(event, wrapFun); return this; } }
function Foo() { getName = function() { alert(1); }; return this; } Foo.getName = function() { alert(2); }; Foo.prototype.getName = function() { alert(3); }; var getName = function() { alert(4); }; function getName() { alert(5); } //请写出如下输出结果: Foo.getName(); // 2 getName(); // 4 Foo().getName(); // 1 getName(); // 1 new Foo.getName(); // 2 new Foo().getName(); // 3 new new Foo().getName(); // 3
详细资料能够参考:
《前端程序员常常忽视的一个 JavaScript 面试题》
《一道考察运算符优先级的 JavaScript 面试题》
《一道常被人轻视的前端 JS 面试题》
Performance API 用于精确度量、控制、加强浏览器的性能表现。这个 API 为测量网站性能,提供之前没有办法作到的精度。 使用 getTime 来计算脚本耗时的缺点,首先,getTime方法(以及 Date 对象的其余方法)都只能精确到毫秒级别(一秒的千分之一),想要获得更小的时间差异就无能为力了。其次,这种写法只能获取代码运行过程当中的时间进度,没法知道一些后台事件的时间进度,好比浏览器用了多少时间从服务器加载网页。 为了解决这两个不足之处,ECMAScript 5引入“高精度时间戳”这个 API,部署在 performance 对象上。它的精度能够达到1毫秒 的千分之一(1秒的百万分之一)。 navigationStart:当前浏览器窗口的前一个网页关闭,发生 unload 事件时的 Unix 毫秒时间戳。若是没有前一个网页,则等于 fetchStart 属性。 loadEventEnd:返回当前网页 load 事件的回调函数运行结束时的 Unix 毫秒时间戳。若是该事件尚未发生,返回 0。
根据上面这些属性,能够计算出网页加载各个阶段的耗时。好比,网页加载整个过程的耗时的计算方法以下:
var t = performance.timing; var pageLoadTime = t.loadEventEnd - t.navigationStart;
详细资料能够参考:
《Performance API》
(1)第一个字符必须是字母、下划线(_)或美圆符号($) (2)余下的字符能够是下划线、美圆符号或任何字母或数字字符 通常咱们推荐使用驼峰法来对变量名进行命名,由于这样能够与 ECMAScript 内置的函数和对象命名格式保持一致。
详细资料能够参考:
《ECMAScript 变量》
在 ECMAScript 规范中,语句结尾的分号并非必需的。可是咱们通常最好不要省略分号,由于加上分号一方面有 利于咱们代码的可维护性,另外一方面也能够避免咱们在对代码进行压缩时出现错误。
Object.assign() 方法用于将全部可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
Math.ceil() === 向上取整,函数返回一个大于或等于给定数字的最小整数。 Math.floor() === 向下取整,函数返回一个小于或等于给定数字的最大整数。
for (var i = 0, j = 0; i < 5, j < 9; i++, j++) { console.log(i, j); } // 当判断语句含有多个语句时,以最后一个判断语句的值为准,所以上面的代码会执行 10 次。 // 当判断语句为空时,循环会一直进行。
咱们须要思考的问题:该处理是否必须同步完成?数据是否必须按顺序完成? 解决办法: (1)将数据分页,利用分页的原理,每次服务器端只返回必定数目的数据,浏览器每次只对一部分进行加载。 (2)使用懒加载的方法,每次加载一部分数据,其他数据当须要使用时再去加载。 (3)使用数组分块技术,基本思路是为要处理的项目建立一个队列,而后设置定时器每过一段时间取出一部分数据,而后再使用定时器取出下一个要处理的项目进行处理,接着再设置另外一个定时器。
在前端实现中咱们通常经过 setTimeout 和 setInterval 方法来实现一个倒计时效果。可是使用这些方法会存在时间误差的问题,这是因为 js 的程序执行机制形成的,setTimeout 和 setInterval 的做用是隔一段时间将回调事件加入到事件队列中,所以事件并非当即执行的,它会等到当前执行栈为空的时候再取出事件执行,所以事件等待执行的时间就是形成偏差的缘由。 通常解决倒计时中的偏差的有这样两种办法: (1)第一种是经过前端定时向服务器发送请求获取最新的时间差,以此来校准倒计时时间。 (2)第二种方法是前端根据误差时间来自动调整间隔时间的方式来实现的。这一种方式首先是以 setTimeout 递归的方式来实现倒计时,而后经过一个变量来记录已经倒计时的秒数。每一次函数调用的时候,首先将变量加一,而后根据这个变量和每次的间隔时间,咱们就能够计算出此时无误差时应该显示的时间。而后将当前的真实时间与这个时间相减,这样咱们就能够获得时间的误差大小,所以咱们在设置下一个定时器的间隔大小的时候,咱们就从间隔时间中减去这个误差大小,以此来实现因为程序执行所形成的时间偏差的纠正。
详细资料能够参考:
《JavaScript 前端倒计时纠偏实现》
详细资料能够参考:
《进程间 8 种通讯方式详解》
《进程与线程的一个简单解释》
function findMostWord(article) { // 合法性判断 if (!article) return; // 参数处理 article = article.trim().toLowerCase(); let wordList = article.match(/[a-z]+/g), visited = [], maxNum = 0, maxWord = ""; article = " " + wordList.join(" ") + " "; // 遍历判断单词出现次数 wordList.forEach(function(item) { if (visited.indexOf(item) < 0) { let word = new RegExp(" " + item + " ", "g"), num = article.match(word).length; if (num > maxNum) { maxNum = num; maxWord = item; } } }); return maxWord + " " + maxNum; }
笔者再次墙裂推荐收藏这个仓库,收录于CavsZhouyou - 🐜 前端面试复习笔记,这个仓库是原做者校招时的前端复习笔记,主要总结一些比较重要的知识点和前端面试问题,但愿对你们有所帮助。
最后若是文章和笔记能带您一丝帮助或者启发,请不要吝啬你的赞和收藏,你的确定是我前进的最大动力 😁