文章目录
- 1、原始值和引用值类型及区别
- 3、“ == ”和“===”的区别
- 2、判断数据类型typeof、instanceof、Object.prototype.toString.call()、constructor
- 3、类数组与数组的区别与转换
- 4、String、Array和Math常见方法
- 5、bind、call、apply的区别与实现
- 6、new的原理,通过new的方式创建对象和通过字面量创建有什么区别?new和Object.create的区别
- 7、如何正确判断this(区别箭头函数)
- 8、严格模式与非严格模式的区别
- 9、原型和原型链
- 10、prototype与__proto__的关系与区别
- 11、继承的实现方式及比较
- 12、class 类、class继承
- 13、作用域和作用域链、执行上下文
- 14、闭包及其作用
- 15、内存管理(内存泄露)
- 16、JS的垃圾回收机制
- 17、深拷贝与浅拷贝区别,如何实现深拷贝
- 18、防抖和节流
- 19、Js事件绑定时,函数名加括号和不加括号区别
- 20、DOM常见的操作方式
- 21、 Array.sort()方法与实现机制
- 19、 Ajax的请求过程
- 22、addEventListener(DOM2级事件处理程序)和onClick()(DOM0级事件处理程序)的区别
- 23、立即执行函数
- 24、整个HTML解析过程(网页渲染过程)
- 25、浏览器的回流(Reflow)和重绘(Repaints)
- 26、EventLoop事件循环(js运行机制)
- 27、setTimeout倒计时为什么会出现误差?
- 28、宏任务与微任务
- 29、DOM的location对象
- 30、跨域、同源策略及跨域实现方式和原理
- 31、浅谈JS变量提升
- 35、JS的map()和reduce()方法
- 37、 函数柯里化及其通用封装
- 38、事件捕获事件冒泡
- 39、js对象属性遍历方法
- 40、for...of , forin 和 forEach,map 的区别
- 41、ES6 新特性
- ES6学习总结
- 42、前端调试方法
- 43、0.1+0.2 != 0.3的问题
- 浏览器内核
1、原始值和引用值类型及区别
在ECMAScript 中,变量可存放里两种类型的值,即原始值和引用值
1、原始值(6种):
原始值指的是代表原始数据类型的值,也叫基本数据类型或简单类型,因为其占据空间固定,是简单的数据段,为了便于提升变量查询速度,将其存储在栈(stack)中,也就是说,他们的值是直接存储在变量访问的位置(按值访问)。包括:
- Undefined,
- Null
- Number
- Boolean
- String
- Symbol(为了解决es5变量名冲突)
2、引用值:
遇到引用值,所处理的就是对象。引用类型由于其值的大小会改变,所以不能直接将其存放在栈中,否则会降低变量查询速度,因此其存储在堆(heap)中,将该对象存储在堆中的地址存储在栈中。该变量是一个指针,指向存储对象的内存处(按址访问)。包括:
- Object
- Function
- Array
- Date
- RegExp
说来也是形象,栈,线性结构,后进先出,便于管理。堆,一个混沌,杂乱无章,方便存储和开辟内存空间
原始值和引用值区别:
- 基本类型的值是不可变得;引用类型的值是可变的
如 调用了toUpperCase()方法后返回的是一个新的字符串,原字符串不变。不能给基本类型添加属性和方法 - 基本类型的比较是值的比较;引用类型的比较是引用的比较
- 基本类型的变量是存放在栈里面的;引用类型的值是保存堆内存中,其在堆中的地址保存在栈中。
null 和 undefined 区别 :
undefined和null的区别
简单来说:null是一个表示"无"的对象(typeOf null == object
),转为数值时为0;undefined是一个表示"无"的原始类型,转为数值时为NaN
null
的使用:
- 定义的变量将来用来保存对象,将其初始化为null
- 当一个数据不再需要使用时,我们最好通过将其值设置为null来释放其引用
undefined
使用:
- 声明一个变量,但没赋值
- 访问对象上不存在的属性
- 函数定义了形参,但没有传递实参
- 函数没有返回值时,默认返回undefined。
js隐式转换类型:
- 转成string类型: +(字符串连接符)
- 转成number类型:
++/--
(自增自减运算符) 、+ - * / %
(算术运算符)> < >= <= == != === !===
(关系运算符) - 转成boolean类型:!(逻辑非运算符)、条件判断
3、“ == ”和“===”的区别
===
不需要进行类型转换,只有类型相同并且值相等时,才返回 true.
==
如果两者类型不同,首先需要进行类型转换。具体流程如下:
- 首先判断两者类型是否相同,如果相等,判断值是否相等.
- 如果类型不同,进行类型转换
- 判断比较的是否是
null
或者是undefined
, 如果是, 返回true
. - 判断两者类型是否为
string
和number
, 如果是, 将字符串转换成number
- 判断其中一方是否为
boolean
, 如果是, 将boolean
转为number
再进行判断 - 判断其中一方是否为
object
且另一方为string
、number
或者symbol
, 如果是, 将object
转为原始类型再进行判断
思考: [ ] == ![ ]
我们来分析一下: [ ] == ![ ] 是true还是false?
- 首先,我们需要知道 ! 优先级是高于
==
- 引用类型转换成布尔值都是true,因此![]的是false
- 根据上面的比较步骤中的第五条,其中一方是 boolean,将 boolean 转为 number 再进行判断,false转换成 number,对应的值是 0.
- 根据上面比较步骤中的第六条,有一方是 number,那么将object也转换成Number,空数组转换成数字,对应的值是0.(空数组转换成数字,对应的值是0,如果数组中只有一个数字,那么转成number就是这个数字,其它情况,均为NaN)
0 == 0; 为true
2、判断数据类型typeof、instanceof、Object.prototype.toString.call()、constructor
typeOf
(不能区别对象类型,但是Function
类型能正确判断,typeof null == 'object'
属于遗留bug)instanceOf
(已知是对象类型时使用,后面一定要是Object
类型): 但无法优雅的判断一个值到底属于数组还是普通对象。- 对象的
constructor
(已知是对象类型时用他,在类继承时会出错,默认指向基类) Object.prototype.toString.call()
:通用方法,准确判断- jQuery.type():万能方法
3、类数组与数组的区别与转换
类数组:
- 类数组是一个普通的对象,而真实的数组是一个
Array
类型 - 拥有
length
属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理) - 不具有数组所具有的方法
常见的类数组:
- 函数参数
arguments
Dom
对象列表(比如通过document.getElementsByTag
得到的列表)- jQuery 对象 (比如
$("div")
)
类数组转换为数组方法:
Array.from(arrayLike)
- 拓展运算符
[...likeArrayObj]
Array.prototype.slice.apply(likeArrayObj)
Array.prototype.splice.call(arrayLike, 0)
Array.prototype.concat.apply([], arrayLike)
- 遍历类数组,依次将元素放入一个空数组(for…of)
4、String、Array和Math常见方法
JavaScript中String、Array、Math、Number、Math属性和方法汇总 、 数组常见API
注意区分Array.splice()
方法和String.split()
方法
1、String常见API
字符方法(前两个都接收一个参数,即基于 0 的字符位置):
charAt()
:返回在指定位置的字符。charCodeAt()
:返回在指定的位置的字符的 Unicode 编码。字母.charCodeAt():返回字母的ASCII码。fromCharCode()
:将 Unicode 编码转为字符。将数字转化为字母:String.fromCharCode(num);
字符串方法(均不改变原字符串):
concat()
:连接两个或更多字符串,不修改原字符串,并返回新的字符串。(更推荐使用“+”加号操作符)slice()
:提取字符串的某个部分,返回被提取的部分。- stringObject.slice(start【,end】),不含end
substr(start,length)
:在字符串中抽取从start下标开始的指定数目的字符。substring(start,stop)
:提取字符串中介于两个指定下标之间的字符。trim()
:创建一个字符串的副本,删除前置及后缀的所有空格,然后返回结果
字符串位置方法:
indexOf()
:返回某个指定的字符串值在字符串中首次出现的位置。- stringObject.indexOf(searchvalue【,fromindex】)。
lastIndexOf()
:返回某个指定的字符串值在字符串中最后一次出现的位置。
大小写转换方法(针对地区和不针对地区):
- toLowerCase() 和 toLocaleLowerCase():把字符串转换为小写
- toUpperCase() 和 toLocaleUpperCase():把字符串转换为大写
字符串的模式匹配方法:
- match():接受一个参数,正则表达式或RegExp 对象,返回一个数组(类似exec)。
- search():接受参数与match同,返回字符串中第一个匹配项的索引,没有返回-1。
- replace():接受两个参数:第 一个参数可以是一个 RegExp 对象或者一个字符串(这个字符串不会被转换成正则表达式),第二个参数可以是一个字符串(替换文本)或者一个函数。
split()
:根据某个字符把字符串拆分成指定长度的数组- stringObject.split(separator【,howmany】)。howmany可选,指定返回的数组的最大长度。如果第一个参数是一个空字符
''
,会返回一个单字符组成的数组
6.localeCompare():比较两个字符串。 如果字符串在字母表中应该排在前面,返回一个负数。等于返回0,之后返回正数。大写排在小写前面。
slice
和substring
区别:
- slice:如果 start 为负,将它作为 length + start处理,此处 length 为数组的长度。如果 end 为负,就将它作为 length + end 处理,此处 length 为数组的长度。如果省略 end ,那么 slice 方法将一直复制到 arrayObj 的结尾。如果 end 出现在 start 之前,不复制任何元素到新数组中
- substring:如果 start 或 end 为 NaN 或者负数,那么将其替换为0。子字符串的长度等于 start 和 end 之差的绝对值。例如,在 strvar.substring(0, 3) 和 strvar.substring(3, 0) 返回的子字符串的的长度是 3。slice可以对数组操作,substring不行。
2、数组API
检测数组
instanceof
操作符:value instanceof Array。它假定只有一个全局执行环境。如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的 Array 构造函数。Array.isArray(value)
:ECMAScript 5新增这个方法的目的是终确定某 个值到底是不是数组,而不管它是在哪个全局执行环境中创建的。
转换方法(如果数组中的某一项的值是 null 或者 undefined,在返回的结果中以空字符串表示。)
toString()
:返回由数组中每个值的字符串形式拼接而成的一个以逗号分隔的字符串。valueOf()
:返回数组对象的原始值。实际上,为了创建这个字符串会调用数组每一项的 toString()方法。join()
方法:arrayObject.join(separator);separator可选。返回一个字符串,指定要使用的分隔符。如果省略该参数,则使用逗号作为分隔符。参数为·''
则没有分割符
栈方法(直接修改原数组)
push()
方法:可向数组的末尾添加一个或多个元素,并返回新的长度
。pop()
方法:用于删除并返回数组的最后一个元素。
队列方法(直接修改原数组)
shift()
方法:用于把数组的第一个元素从其中删除,并返回第一个元素的值。unshift()
方法:可向数组的开头添加一个或更多元素,并返回新的长度。
重排序方法(直接修改原数组)
reverse()
方法:用于颠倒数组中元素的顺序sort()
方法:调用每个数组项的 toString()转型方法,然后比较得到的字符串,以确定如何排序。(需要一个函数辅助才能正确排列顺序)
操作方法
concat()
方法:用于连接两个或多个数组。不会改变现有的数组,返回被连接数组的一个副本。slice()
方法:从已有的数组中返回选定的元素。slice(startIdx,endIdx);返回数组不包含end。不会修改数组。splice()
方法:向/从数组中添加/删除项目,返回被删除的项目。改变原始数组。splice(index,howmany,item1,…,itemX);如果是插入操作,则在指定位置的前面开始插入
位置方法(都接收两个参数:要查找的项和(可选)表示查找起点位置的索引)
indexOf()
和lastIndexOf()
:返回要查找的项在数组中的位置,在没找到的情况下返回-1,查找的项必须严格相等includes
:(ES6新增的数组方法,为了完善indexOf方法不能识别NaN)判断数组是否包含某个值,返回布尔值
迭代方法(不会修改数组中的包含的值)
every()
:对数组中的每一项运行给定函数,如果该函数对每一项都返回 true,则返回 true;some()
:对数组中的每一项运行给定函数,至少有1项返回true,则返回 true;filter()
:对数组中的每一项运行给定函数,返回该函数会返回 true 的项组成的数组;map()
:对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组;forEach()
:对数组中的每一项运行给定函数。这个方法没有返回值,返回undefined。
ES6 提供三个新的方法 —— entries()
,keys()
和values()
—— 用于遍历数组。它们都返回一个遍历器对象,可以用for...of
循环进行遍历,唯一的区别是keys()
是对键名的遍历、values()
是对键值的遍历,entries()
是对键值对的遍历
5、bind、call、apply的区别与实现
《[面试篇]寒冬求职季之你必须要懂的原生JS(上)》、《js中call、apply、bind那些事》、《手写 call、apply 及 bind 函数》
他们都是是函数的一个方法,作用是改变函数运行时this的指向(函数执行时上下文),call
和aplly
和bind
的第一个参数都是将作为它运行时的 this
call、apply的区别:
- 在于参数的区别
- call从第二个参数开始以参数列表的形式展现
fn.call(obj, arg1, arg2, arg3...);
- apply第二个参数是一个数组
fn.apply(obj, [arg1, arg2, arg3...]);
call、apply与bind的差别:
- call和apply改变了函数的this上下文后便执行该函数,而bind则是返回改变了上下文后的一个函数(还没执行)。
应用:
- 求数组中的最大和最小值
Math.max.apply(this, arr)
- 将伪数组转化为数组:
Array.prototype.slice.call(arrayLike);
- 数组追加
[].push.apply(arr1, arr2);
返回改变了的arr1
- 利用call和apply做继承
- 判断变量类型
Object.prototype.toString.call(obj)
6、new的原理,通过new的方式创建对象和通过字面量创建有什么区别?new和Object.create的区别
1、new的原理
- 创建一个新的空对象
- 将新对象的
__proto__
属性指向构造函数的prototype
原型属性 - 将 this 指向这个新对象并执行构造函数中的代码(为这个新对象添加属性)
- 判断构造函数的返回值类型,如果是值类型,返回新创建的对象。如果是引用类型,就返回这个引用类型的对象
new的实现
function new(func) {
let target = {};
target.__proto__ = func.prototype; // 也可以说是添加原型属性
let res = func.call(target); // 添加实例属性
if (res && typeof(res) == "object" || typeof(res) == "function") {
return res;
}
return target;
}
2、new Object()和{…}区别:
添加链接描述
字面量创建对象{...}
,不会调用 Object构造函数, 简洁且性能更好,因为不需要作用域解析。假如存在我们创建了一个同名构造函数Object()的可能,所以当我们调用Object()的时候,解析器需要顺着作用域链从当前作用域开始查找,如果在当前作用域找到了名为Object()的函数就执行,如果没找到,就继续顺着作用域链往上找,直到找到全局Object()构造函数为止
3、Object.create和new的区别:
Object.create和new的区别
js中创建对象的方式一般有两种Object.create和new
Object.create(proto,[propertiesObject])
- proto:新创建对象的原型对象
- propertiesObject:可选。要添加到新对象的可枚举(新添加的属性是其自身的属性,而不是其原型链上的属性)的属性。
注意Object.create()
传入的参数是一个对象。如果传入的是一个构造函数的话,该实例是无法继承的。
Object.create的实现方式
Object.create = function (Base) {
var F = function () {};
F.prototype = Base;
return new F();
};
很多源码作者会使用Object.create(null)
来初始化一个新对象而不是直接用{}
来创建
- 使用create创建的对象,没有任何属性,显示No properties,我们可以把它当作一个非常纯净的map来使用,我们可以自己定义
hasOwnProperty
、toString
方法
7、如何正确判断this(区别箭头函数)
普通函数,this的指向在函数执行的时候是确定的,实际上this的最终指向的是那个调用它的对象(箭头函数中的this是在定义函数的时候绑定,而不是在执行函数的时候绑定)
- 全局作用域或者普通函数中 this 指向全局对象 window。
- 方法调用中谁调用 this 指向谁
- 在构造函数或者构造函数原型对象中 this 指向构造函数的实例
- call谁就是谁,apply谁就是谁,bind谁就指向谁,其实bind就是通过call和apply实现的
箭头函数:
- 没有arguments类数组
- 没有原型prototype,不能new,不能用作构造函数
- this指向是在定义时确定,继承自外层第一个普通函数的this
- 只能通过
call、apply、bind
改变它外层的this来改变箭头函数的上下文(因为就是继承外层的this的嘛)它本身是不能被改变的
严格模式下的this相对于非严格模式下的this的主要区别在于:
- 对于没有写执行主体的方法情况下,非严格模式默认都是window执行的,所以this指向的是
window
,但是在严格模式下,没有写执行主体,this指向是undefined
;
箭头函数不适合使用场景:
什么时候不能使用箭头函数
- 定义对象方法:在一个对象内定义一个方法属性,该方法属性被调用时this指向所属的对象
- 定义事件回调函数:
addEventListener
给一个DOM节点添加事件处理程序,里面的方法要指向绑定的节点 - 定义原型方法:需要在运行时才确定this,因为有不同实例
- 定义构造函数:没有原型,本身没有this,不能new
8、严格模式与非严格模式的区别
参考文献、MDN严格模式
严格模式对 js 的使用添加了一些限制,使代码更合理、更安全、更严谨
1、首先,严格模式通过抛出错误来消除一些人为操作的失误(之前是不报错也不产生效果)
- 严格模式下无法再意外创建全局变量(变量名拼写错误)即不允许给未声明的变量赋值
- 在严格模式下, 试图删除不可删除的属性时会抛出异常(之前只是不产生效果,不报错)
- 严格模式下,重名属性被认为是语法错误(之前只是会覆盖,不报错)
2、严格模式提早设置了一些限制来减轻之后版本改变产生的影响,为未来的ECMAScript版本铺平道路
- 在严格模式中一部分字符变成了保留的关键字(之后的版本可能是关键字)
3、其次,严格模式修复了一些导致JavaScript引擎难以执行优化的缺陷:有时候,相同的代码,严格模式可以比非严格模式下运行的更快。
9、原型和原型链
原型:
当创建一个函数时,该函数就有一个原型属性prototype
(反过来具有该属性的对象就是一个函数),该属性指向一个对象(原型对象),原型对象里面有constructor
指向该函数。原型对象里面的属性是所有实例对象都可以共享的。
原型链:
原型链主要是解决继承问题。每个对象都有一个__proto__
属性,该属性指向构造函数的原型对象并从中继承构造函数的原型方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null(Object.prototype.__proto__
指向的是null)。这种关系被称为原型链 (prototype chain),通过原型链一个对象可以拥有定义在其他对象中的属性和方法。
10、prototype与__proto__的关系与区别
JS中的prototype、__proto__与constructor
prototype与__proto__区别
prototype
是构造函数的属性。只有函数才有此属性
__proto__
是每个实例都有的属性,可以访问 [[prototype]]
属性。
实例的__proto__
与其构造函数的prototype
指向的是同一个
11、继承的实现方式及比较
继承方式 | 继承实现 | 特点 |
---|---|---|
原型链继承 | 子类的原型属性prototype 指向父类的实例 Son.prototype = new Farther() | 可继承父类构造函数属性、原型属性;但是子类实例无法向父类构造传参;所有子类新实例都会共享父类实例的属性 |
借用构造函数继承 | 用call 或apply 将父类构造函数引入子类构造函数中 | 无法继承父类原型属性;方法都在构造函数中定义,每次创建实例都会创建一遍父类方法;可以继承多个不同构造函数属性(call多个)。子类实例可向父类构造函数传参;避免了父类的属性被所有子类实例共享 |
组合继承(组合原型链继承和借用构造函数继承)常用 | 结合了两种模式的优点;可传参、子类实例不会共享父类属性 | 调用了两次父类构造函数(耗内存) |
原型式继承 | 其实原理和原型链继承一样,只不过这个是用一个函数封装好,将父类实例作为参数传入,函数内部将新建一个构造函数函数,并把其原型prototype 指向父类实例,最后返回新建构造函数的实例 | 和Object.create 原理一样;优缺点同原型链继承,即所有实例都会继承原型上的属性等 |
寄生式继承 | 就是给原型式继承外面套了个壳子,封装好用传参方式 | 缺点跟借用构造函数模式一样,每次创建对象都会创建一遍父类方法 |
寄生组合继承常用 | 寄生:在函数内返回对象然后调用;组合:1、函数的原型等于另一个实例。2、在函数中用apply或者call引入另一个构造函数,可传参 | 它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof |
12、class 类、class继承
《在使用es6语法class的时候,babel到底做了什么》
- ES6 新添加的 class 只是为了补充 js 中缺少的一些面向对象语言的特性,但本质上来说它只是一种语法糖,不是 一个新的东西,其背后还是原型继承的思想。
class
的作用就是让JavaScript引擎去实现原来需要我们自己编写的原型链代码。简而言之,用class的好处就是极大地简化了原型链代码 - ES6创建一个class会默认添加
constructor
方法,并在new调用时自动调用该方法 - 在 class 中添加的方法,其实是添加在类的原型上的。
- 不是所有的主流浏览器都支持ES6的class,要使用
babel
转换为传统的prototype
代码
class继承:
- 使用
extends
则表示原型链对象来自父类 - 如果子类想要继承父类的方法,同时在自己内部扩展自己的方法,利用super 调用父类的构造函数,super 必须在子类this之前调用
- 子类必须在
constructor
方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
13、作用域和作用域链、执行上下文
看 《红宝书》 第4章 73 页、178 页
JavaScript作用域、上下文、执行期上下文、作用域链、闭包
执行上下文就是当前 JavaScript 代码被解析和执行时所在环境, JS执行上下文栈可以认为是一个存储函数调用的栈结构,遵循先进后出的原则。
- JavaScript执行在单线程上,所有的代码都是排队执行。
- 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
- 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行-完成后,当前函数的执行上下文出栈,并等待垃圾回收。
- 浏览器的JS执行引擎总是访问栈顶的执行上下文。
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈。
作用域链: 无论是 LHS 还是 RHS 查询,都会在当前的作用域开始查找,如果没有找到,就会向上级作用域继续查找目标标识符,每次上升一个作用域,一直到全局作用域为止。
14、闭包及其作用
什么是闭包?以及闭包的优点,缺点,用处,及特性
JS高程中关于闭包与变量
JavaScript中匿名函数循环传参数(不触发函数的执行)
如何用 Chrome 解决内存泄漏
什么是闭包:
闭包就是指有权访问另一个函数作用域中的变量的函数,简单来说就是函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。
闭包特点:
- 函数嵌套函数
- 函数内部可以引用外部的参数和变量
- 参数和变量不会被垃圾回收机制回收
闭包作用优点:
- 函数执行完之后不会立即销毁变量及其作用域,因为闭包函数保留了变量对象的引用
- 创建私有变量:通过函数返回一个闭包函数,这个闭包可以访问到函数内部的变量,因此我们可以在函数外就可以访问了函数内部变量
- 因此可以避免了全局变量的污染;(防抖节流)
闭包缺点:
常驻内存 会增大内存的使用量 使用不当会造成内存泄露
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值
闭包使用场景:
- 防抖节流(
超级经典
):(在防抖节流
中,这个timer变量当然也可以写在全局作用域中,但是可能会跟全局作用域中的变量产生冲突,所以在这里用闭包的形式来提供,防止它污染全局作用域)
15、内存管理(内存泄露)
哪些操作会引起内存泄露:
- 意外的全局变量(一直占用内存,直到关闭浏览器或者认为清除)
- 被遗忘的计时器或回调函数
- 脱离 DOM 的引用
- 闭包
如何检测内存泄露:《内存泄漏及如何避免及检测》
浏览器方法:(可以分别两次记录下快照,再进行对比)
- 打开开发者工具,选择
Memory
选项板; - 在右侧的Select profiling type字段里面勾选
timeline
; - 点击左上角的录制按钮;
- 在页面上进行各种操作,模拟用户的使用情况;
- 一段时间后,点击左上角的 stop 按钮,面板上就会显示这段时间的内存占用情况。
命令行方法:
- 使用Node提供的
process.memoryUsage
方法
解决内存泄露:(解除引用)
一旦数据不再使用,最好通过将其值设为null
来释放其引用
16、JS的垃圾回收机制
《深入理解V8的垃圾回收原理》、 《js内存深入学习(一)》
V8垃圾收集器会每隔一段时间就执行一次释放操作,找出那些不再继续使用的值,然后释放其占用的内存。
- 标记清除(Mark-Sweep ):现代浏览器基本都是用此方法
- 引用计数:低版本的IE使用这种方式;循环引用是致命的问题
V8 引擎的垃圾回收机制(基于分代回收机制):
1、将对象分为新生代、老生代
- 新生代:存活时间短,只经历过一次垃圾回收就被回收了
- 老生代:存活时间长,经过多次垃圾回收仍然存活
V8堆的整体大小就是新生代所用内存空间+老生代所用内存空间
2、 新生代被分为 From
和 To
两个空间
To
一般是处于闲置的。- 当
From
(处于使用中)空间满了的时候会执行Scavenge (清除)
算法进行垃圾回收
3、老生代采用了标记清除法
。标记清除法首先会从全局对象开始对内存中存活的对象进行标记,标记结束后清除掉那些没有标记的对象。
4、由于标记清除后会造成很多的内存碎片,不便于后面的内存分配(比如需要申请大块内存)。所以为了解决内存碎片的问题引入了标记压缩法
。
5、标记压缩:标记清除是对未标记的对象立即进行回收,Mark-Compact则是将标记的对象移动到一边,然后再清理未标记的
ps:进行垃圾回收的时候会暂停应用的逻辑执行
17、深拷贝与浅拷贝区别,如何实现深拷贝
深拷贝与浅拷贝的区别,实现深拷贝的几种方法
聊聊对象深拷贝和浅拷贝
浅拷贝、深拷贝本身只针对较为复杂的object类型数据来说的。
- 浅拷贝是指只复制第一层对象,但是当对象的属性是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。
- 深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。深拷贝后的对象与原来的对象是完全隔离的,互不影响,对一个对象的修改并不会影响另一个对象。
简单来说,就是假设B复制了A,当修改A时,看B是否会发生变化,如何B也跟着变了,则说明是浅拷贝,如果B不受影响,则是深拷贝。
JavaScript中常见的浅拷贝方法(都是只拷贝了第一层属性):
- ES6提供的
Object.assign(target, sourcesObject)
- 拓展运算符
let cloneObj = { ...obj };
Array.prototype.slice(begin, end)
Array.prototype.concat()
数组拼接
深拷贝实现:
JSON.stringify()
和JSON.paese()
(常用)- 借用JQ的
extend(boolean,targetObject,sourceObject)
方法 - 自定义实现
自己实现深拷贝
function deepCopy(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] = typeof object[key] === "object" ? deepCopy(object[key]) : object[key];
}
}
return newObject;
}
18、防抖和节流
闭包的经典应用
这个timer变量当然也可以写在全局作用域中,但是可能会跟全局作用域中的变量产生冲突,所以在这里用闭包的形式来提供,防止它污染全局作用域
在这个函数中,要保存好传进来的执行上下文this
,和参数arguments
。应为我们要注意的是setTimeout()
函数中作用域是全局
的,也就是setTimeout中的this指的是window
,我们需要的上下文是我们所绑定的对象
1、防抖:
所谓防抖,指连续触发的事件在规定的时间内不再触发时,事件才会执行一次,如果在规定的时间内再次触发事件,则会重新计算执行时间
防抖实现(非立即执行和立即执行)
- 非立即执行
function debounce(fn, wait) {
var timeout;
return function() {
if(timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(fn, wait);
}
}
- 立即执行版
function debounce(func,wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
let callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait)
if (callNow) func.apply(context, args)
}
}
防抖应用:
- 每次 resize / scroll / mousemove触发统计事件
- 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)
2、节流:
所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。
节流实现(时间戳和定时器)借助闭包
- 时间戳版(函数会立即执行)
function throttle(func, wait) {
let previous = 0;
return function() {
let now = Date.now(); // 当前时间
let context = this;
let args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now; // 上次执行完的时间(可能是忽略了函数自身执行时间)
}
}
}
- 定时器版:(函数不会立即执行)
function throttle(func, wait) {
let timeout;
return function() {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
func.apply(context, args) // 函数执行,此时定时器还没清空
timeout = null; // 函数执行完之后清空定时器
}, wait)
}
}
}
19、Js事件绑定时,函数名加括号和不加括号区别
20、DOM常见的操作方式
(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)
21、 Array.sort()方法与实现机制
19、 Ajax的请求过程
- 创建
XMLHttpRequest
对象 - 创建一个
http
请求,并制定http
请求的方法、URL - 设置响应
http
请求状态变化的函数 - 发送
http
请求 - 获取异步调用返回的数据并渲染刷新页面
var xmlhttp;
if(window.XMLHttpRequest){
//IE7+,Chrome,Firefox,Safari,Opera执行此代码
xmlhttp=new XMLHttpRequest;
}else{
//IE5,IE6执行该代码
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlHttp.open('GET','demo.php','true');
xmlHttp.send()
xmlHttp.onreadystatechange = function(){
if(xmlHttp.readyState === 4 & xmlHttp.status === 200){
}
}
Ajax 的 readyStates 几种状态:
- 0——初始化
- 1——载入
- 2——载入完成
- 3——解析
- 4——完成
22、addEventListener(DOM2级事件处理程序)和onClick()(DOM0级事件处理程序)的区别
看 《红宝书》 第 350 页
-
DOM0级事件处理程序:每个元素(包括 window 和 document)都有自己的事件处理程序属性,所以他 是将 事件(比如onclick)当作 dom 节点的一个属性来看待,将这个属性的值设置为一个函数,就可以给相应的 dom 节点指定事件处理程序
-
DOM2级事件处理程序:定义了两个方法 addEventListener() 和 removeEventListener() 所有 dom节点都包含这两个方法。区别于 DOM0 级事件处理程序,他可以为 dom 节点添加多个事件处理程序,相应的程序会按照添加他们的顺序依次触发。通过
addEventListener()
添加的事件处理程序只能使用removeEventListener()
来移除,移除时传入的参数与添加处理程序时使用的参数相同,因此通过 addEventLisenter() 添加匿名函数将无法移除
Dom0 和 DOM2 事件出来了程序都是在其依附的元素的作用域中运行的,此时 this 指向当前元素,但是IE中使用的 attachEvent()
事件处理程序会在全局作用域中运行,因此 this 指向 window
23、立即执行函数
深入理解js立即执行函数、什么是立即执行函数,它有什么作用?
使用立即执行函数的作用(为了避免变量污染即命名冲突):
- 不必为函数命名,避免了污染全局变量
- 立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量
实际使用场景:
- 你的代码在页面加载完成之后,不得不执行一些设置工作,比如时间处理器,创建对象等等。
- 所有的这些工作只需要执行一次,比如只需要显示一个时间。
- 但是这些代码也需要一些临时的变量,但是初始化过程结束之后,就再也不会被用到,如果将这些变量作为全局变量,不是一个好的注意,我们可以用立即执行函数——去将我们所有的代码包裹在它的局部作用域中,不会让任何变量泄露成全局变量
jQuery源码开篇用的就是立即执行函数。立即执行函数常用于第三方库,好处在于隔离作用域,任何一个第三方库都会存在大量的变量和函数,为了避免变量污染(命名冲突),开发者们想到的解决办法就是使用立即执行函数。
通过定义一个匿名函数,创建了一个新的函数作用域,相当于创建了一个‘私有’的命名空间,该命名空间的变量和方法,不会破坏污染全局的命名空间。 此时若是想访问全局对象,将全局对象以参数形式传进去即可
jQuery的代码结构
(function( window, undefine ) {
jQuery code
})(window)
其中window即是全局对象。作用域隔离非常重要,是一个JS框架必须支持的功能,jQuery被应用在成千上万的JavaScript程序中,必须确保jQuery创建的变量不能和导入他的程序所使用的变量发生冲突。
24、整个HTML解析过程(网页渲染过程)
《JS脚本解析与执行顺序》、《浏览器如何渲染页面》、《css会阻塞页面dom解析吗javascript呢》、《CSS 和 JS 阻塞二三事》、《偏底层的理解》、《浏览器与Node的事件循环有何区别》
浏览器内核(渲染进程):
- 浏览器内有多个进程,其中渲染进程被称为浏览器内核,渲染进程负责浏览器的解析和渲染,内部有 JS 引擎线程(V8)、 GUI 渲染线程、事件循环管理线程、定时器线程、HTTP 线程。
- JS 引擎线程负责执行 JS 脚本,GUI 渲染线程负责页面的解析和渲染,两者是互斥的,也就是执行 JS 的时候页面是停止解析和渲染的。(避免不必要的渲染和冲突)
- JS 要操作 DOM ,就要涉及 JS 引擎线程和 GUI 渲染线程的通信,而线程间通信代价是非常昂贵的,因为线程上下文切换消耗性能上的开销,这也是造成 JS 操作 DOM 效率不高的原因。
浏览器渲染步骤:《渲染树怎么形成的你真的很懂吗?》
1、 将HTML
字节流解析成 DOM Tree
- 词法分析:把字符流(HTML其实是一坨字符串)初步解析成我们可理解的"词",学名叫token(一个数组,有点像
babel
转化) - 语法分析:将 Token 解析为 DOM 节点(把开始结束标签配对、属性赋值好、父子关系连接好、构成dom树)(利用了栈数据结构)
2、同时将 CSS
解析成CSSOM Tree
(不冲突)
3、将 DOM Tree
和 CSSOM Tree
合并成Render Tree
4、生成布局Layout
,计算Render Tree
中各节点元素的尺寸、位置(自动重排)
5、将Render Tree
绘制成像素点,调用由浏览器的UI组件的paint()
方法在屏幕上显示对应的内容
render tree
类似于DOM
树,但区别很大,render tree
能识别样式,render tree
的每一个节点都有自己的样式,而且render tree
中不包含隐藏的节点(比如display:none
的节点,还有head
节点),因为这些节点不会用于呈现,而且不会影响呈现
拓展:
首先会生成Render Tree
,其实这还是 16 年之前的事情,现在 Chrome
团队已经做了大量的重构,已经没有生成Render Tree的过程了。而布局树的信息已经非常完善,完全拥有Render Tree的功能
CSS阻塞:
- 不管是内联还是外链的css 文件的
下载
和解析
不会影响 DOM 的解析,但是会阻塞 DOM 的渲染。因为 CSSOM Tree 要和 DOM Tree 合成 Render Tree 才能绘制页面。 - CSS文件没下载并解析完成之前,后续的 js 脚本不能执行
- 所以css文件一般放在HTML文档的
<head>
标签内,不是放在HTML
文档的底部
JS阻塞:
- 内联还是外链js 文件的
下载
和解析
都会阻塞 GUI 渲染进程,也就是会阻塞 DOM 和 CSS 的解析和渲染 - 所以把
JS
文件放在页面底部更有利于页面解析和渲染
总结:
- 不管是CSS的下载和解析、JS的下载和解析,都会阻塞页面渲染
- 但只有JS的下载和解析,才会阻塞DOM的解析
defer、async情况:
defer
:表示延迟
执行引入的 JS,即在下载
js文件期间不会阻塞HTML的解析,这两个过程是并行的。当整个 document 解析完毕后再执行脚本文件,在DOMContentLoaded
事件触发之前完成。多个脚本按顺序执行
async
:表示异步执行引入的 JS,与 defer 的区别在于,如果已经加载好,就会开始执行,也就是说它的执行仍然会阻塞文档的解析,只是它的加载过程不会阻塞。多个脚本的执行顺序无法保证
load和DOMcontentLoaded区别:
- load 是所有资源加载完毕(图片,DOM,音视频,脚本等)后触发domcontent是DOM树构建完成触发
- load 所有浏览器都支持domcontent 不同浏览器对其支持不同,需要兼容
Css为什么要放在HTML文档的标签内:
- 如果把css文件引用放在HTML文档的底部,浏览器为了防止无样式内容
闪烁
,会在css文件下载并解析完毕之前什么都不显示,这也就会造成白屏现象
。(但是在firefox浏览器中测试,会出现样式闪烁,这也算是不同浏览器的权衡吧,要么等css全解析完一起显示,要么先显示然后css解析完再重新画上新样式) - 当css文件放在head中时,虽然css解析也会阻塞后续dom的渲染,但是在解析css的同时也在解析dom,所以等到css解析完毕就会逐步的渲染页面了。
我们为什么一再强调将css放在头部,将js文件放在尾部:
-
在面试的过程中,经常会有人在回答页面的优化中提到将js放到body标签底部,原因是因为浏览器生成Dom树的时候是一行一行读HTML代码的,script标签放在最后面就不会影响前面的页面的渲染。那么问题来了,既然Dom树完全生成好后页面才能渲染出来,浏览器又必须读完全部HTML才能生成完整的Dom树,script标签不放在body底部是不是也一样,因为dom树的生成需要整个文档解析完毕。
- -
我们再来看一下chrome在页面渲染过程中的,绿色标志线是First Paint的时间。纳尼,为什么会出现firstpaint,页面的paint不是在渲染树生成之后吗?其实现代浏览器为了更好的用户体验,渲染引擎将尝试尽快在屏幕上显示的内容。它不会等到所有HTML解析之前开始构建和布局渲染树。部分的内容将被解析并显示。也就是说浏览器能够渲染不完整的dom树和cssom,尽快的减少白屏的时间。假如我们将js放在header,js将阻塞解析dom,dom的内容会影响到First Paint,导致First Paint延后。所以说我们会将js放在后面,以减少First Paint的时间,但是不会减少DOMContentLoaded被触发的时间
25、浏览器的回流(Reflow)和重绘(Repaints)
《面试总结回流重绘》、《浅析网页中的回流和重绘》、《页面优化,谈谈重绘和回流》
浏览器对页面的呈现的处理流程:
-
浏览器把获取到的HTML代码解析成一棵DOM树,HTML中的每个标签(tag)都是DOM树中的一个节点,根节点就是我们常用的document对象。DOM树里包含了HTML所有标签,包括display:none隐藏,还有用JS动态添加的元素等;
-
浏览器把所有样式(用户定义的css和用户代理)解析成样式结构体,在解析过程中会去掉浏览器不能识别的样式,比如IE会去掉-moz开头的样式,而Firefox会去掉_开头的样式;
-
DOM树和样式结构体组合后构建render tree(渲染树),render tree类似于DOM树,但区别很大,render tree 能识别样式,render tree的每一个节点都有自己的样式,而且render tree中不包含隐藏的节点(比如display:none的节点,还有head节点),因为这些节点不会用于呈现,而且不会影响呈现。
注意:visibility:hidden隐藏的元素还是会包含到render tree中,因为visibility:hidden会影响布局(layout),会占有空间。根据css2的标准,render tree中的每个节点都称为Box(Box demensions),理解页面元素为一个具有填充,边距,边框和位置的盒子。
总结:
- 引起DOM树结构变化,页面布局变化的行为叫回流,且回流一定伴随重绘。
- 只是样式外观的变化,不会引起DOM树变化,页面布局变化的行为叫重绘,且重绘不一定会便随回流。
- 回流往往伴随着布局的变化,代价较大,重绘只是样式的变化,结构不会变化。操作dom会引起回流重绘,从而
消耗性能
触发回流:
- 添加或者删除可见的 DOM 元素;
- 元素尺寸改变——边距、填充、边框、宽度和高度
- 内容变化,比如用户在 input 框中输入文字
- 浏览器窗口尺寸改变——resize事件发生时
- 计算 offsetWidth 和 offsetHeight 属性
- 设置 style 属性的值
- 当你修改网页的默认字体时。
实践意义(优化):
- 不要一条一条地修改 DOM 的样式。而是预先定义好 css 的 class,然后修改 DOM 的
className
。 - DOM 离线修改:使用
DocumentFragment
进行批量的 DOM 操作(这样不触发回流) - 对于
resize
、scroll
等进行防抖/节流处理 - 使用
transform
替代top
(让渲染引擎为其单独实现一个图层,当这些变换发生时,仅仅只是利用合成线程去处理这些变换,而不牵扯到主线程,大大提高渲染效率) - 尽量少使用
table
布局,可能很小的一个小改动会造成整个 table 的重新布局
注意点:
- GUI 渲染线程会尽可能早的将内容呈现到屏幕上,并不会等到所有的 HTML 都解析完成之后再去构建和布局 Render Tree,而是解析完一部分内容就渲染一部分内容,同时,可能还在通过网络下载其余内容。
大量插入dom元素的方法:
- 一般情况在一个
for
循环中使用appendChild
:每次都会发生回流重绘,性能差 - 文档碎片
DocumentFragment
的机制:把所有要构造的节点都放在文档片段中执行,这样可以不影响文档树,也就不会造成页面渲染。当节点都构造完成后,再将文档片段对象添加到页面中,这时所有的节点都会一次性渲染出来,
因为DocumentFragment
不是真实 DOM 树的一部分,它的变化不会触发 DOM 树的重新渲染
<ul id="root"></ul>
<script>
var root = document.getElementById('root')
var fragment = document.createDocumentFragment()
for(let i = 0; i < 1000; i++){
let li = document.createElement('li')
li.innerHTML = '我是li标签'
fragment.appendChild(li)
}
root.appendChild(fragment);
</script>
26、EventLoop事件循环(js运行机制)
《这一次,彻底弄懂 JavaScript 执行机制》、《彻底搞懂浏览器Event-loop》、《阮一峰的JavaScript 运行机制详解:再谈Event Loop》、《event loop、进程和线程、任务队列》、《浏览器与Node的事件循环有何区别》
众所周知,浏览器的js是单线程的,也就是说,在同一时刻,最多也只有一个代码段在执行,可是浏览器又能很好的处理异步请求,那么到底是为什么呢?讲讲浏览器线程:
浏览器内核(渲染进程)是多线程,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:
- JavaScript引擎线程(V8):主线程,也可以说是主执行栈,我们平时说js是单线程就是指它。该线程与 GUI渲染线程互斥,当 JS引擎线程执行 JavaScript脚本时间过长,将导致页面渲染的阻塞。
- GUI 渲染线程:主要负责页面的渲染,解析HTML、CSS,构建DOM树,布局和绘制等。
- 定时触发器线程:负责执行异步定时器一类的函数的线程,如: setTimeout,setInterval。主线程依次执行代码时,遇到定时器,会将定时器交给该线程处理,当计数完毕后,事件触发线程会将计数完毕后的事件加入到任务队列的尾部,等待JS引擎线程执行。
- 事件触发线程:主要负责将准备好的事件交给 JS引擎线程执行。ajax等异步请求成功并触发回调函数,或者用户触发点击事件时,该线程会将整装待发的事件依次加入到任务队列的队尾,等待 JS引擎线程的执行。
- 异步http请求线程:负责执行异步请求一类的函数的线程,如: Promise,axios,ajax等。主线程依次执行代码时,遇到异步请求,会将函数交给该线程处理,当监听到状态码变更,如果有回调函数,事件触发线程会将回调函数加入到任务队列的尾部,等待JS引擎线程执行。
js引擎线程是主线程,其他的线程是工作线程(web worker
)。
- 因为js是单线程运行的,所以在代码执行的过程中,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
- 首先在执行同步代码的时候,如果遇到了异步事件(比如访问一个接口),js引擎会将这个异步事件挂起,继续执行执行栈中的下面代码。当异步任务执行完毕后,会将异步事件对应的回调函数(处理后台返回的数据)加入到队列中,
这就是为什么异步任务无法保证回调函数的执行顺序,哪个异步任务执行快,将会优先其回调函数放入队列执行
,队列又分为微任务队列,宏任务队列。 - 当当前执行栈中的事件执行完毕后,js引擎首先会判断微任务队列中是否有任务可执行,如果有则将其队首的事件压入执行栈中执行。
- 执行完微任务队列中的所有任务(注意是微任务队列的全部任务)后即为一个事件循环
- 一般执行完一个事件循环 Tick之后,就会执行渲染操作,更新界面。
- 接着再去判断宏任务队列中是否有任务,开启新的一个事件循环
27、setTimeout倒计时为什么会出现误差?
setTimeout() 只是将事件插入了“任务队列”,必须等当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码消耗时间很长,也有可能要等很久,所以并没办法保证回调函数一定会在 setTimeout() 指定的时间执行。所以, setTimeout() 的第二个参数表示的是最少时间,并非是确切时间。
HTML5标准规定了 setTimeout() 的第二个参数的最小值不得小于4毫秒,如果低于这个值,则默认是4毫秒。在此之前。老版本的浏览器都将最短时间设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常是间隔16毫秒执行。这时使用 requestAnimationFrame() 的效果要好于 setTimeout();
28、宏任务与微任务
- 宏任务:包括整体代码script,setTimeout,setInterval.,setImmediate
- 微任务:Promise.then,Promise.catch, process.nextTick
29、DOM的location对象
30、跨域、同源策略及跨域实现方式和原理
前端常见跨域解决方案(全)
跨域的几种常见的解决方式
为什么要有跨域限制
31、浅谈JS变量提升
var str; //这个属于变量声明
str = "hhh"; //这个属于变量定义
另
var str2="fff";
这样的其实是两个过程,可以看成
var str2;
str2="fff";
而变量声明是会被提升的
35、JS的map()和reduce()方法
37、 函数柯里化及其通用封装
-
柯里化,其实就是高阶函数的一种特殊用法,柯里化是指这样一个函数(假设叫做createCurry),他接收函数A作为参数,运行后能够返回一个新的函数。并且这个新的函数能够处理函数A的剩余参数。
-
柯里化函数的运行过程其实是一个参数的收集过程,我们将每一次传入的参数收集起来,并在最里层里面处理。在实现createCurry时,可以借助这个思路来进行封装
-
柯里化确实是把简答的问题复杂化了,但是复杂化的同时,我们使用函数拥有了更加多的自由度。而这里对于函数参数的自由处理,正是柯里化的核心所在
函数柯里化的封装:
// 简单实现,参数只能从右到左传递
function createCurry(func, args) {
var arity = func.length;
var args = args || [];
return function() {
var _args = [].slice.call(arguments);
[].push.apply(_args, args);
// 如果参数个数小于最初的func.length,则递归调用,继续收集参数
if (_args.length < arity) {
return createCurry.call(this, func, _args);
}
// 参数收集完毕,则执行func
return func.apply(this, _args);
}
}
举一个非常常见的例子。
当我们需要验证电话、邮箱、身份证等等用户信息的时候,按照普通的思路就是写一个一个的验证函数,稍微好一点的,我们就会封装一个更为通用的函数,将用于验证的正则与将要被验证的字符串作为参数传入。但是这样封装之后,在使用时又会稍微麻烦一点,因为会总是输入一串正则,这样就导致了使用时的效率低下
这个时候,我们就可以借助柯里化,在之前封装函数的基础上再做一层封装,以简化使用
然这个案例本身情况还算简单,但是柯里化在面对复杂情况下的灵活性却让我们不得不爱。
38、事件捕获事件冒泡
事件捕获和事件冒泡都是为了解决页面中的事件流(事件触发的顺序)
- 事件冒泡:
微软提出了事件冒泡事件流,即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档) - 事件捕获:
网景公司提出了事件捕获事件流,刚好和事件冒泡相反,事件会从最不具体,即最外层的那个元素开始发生,层层递进,直到最具体的那个元素。
阻止事件冒泡
- 一般浏览器是直接给子级加
event.stopPropagation( )
- IE10以下的用
e.cancelBubble = true
- 在事件处理函数中
return false;
这个不仅阻止了事件往上冒泡,而且阻止了事件本身(默认事件)
阻止默认事件
- 一般浏览器使用
e.preventDefault()
- IE则是给绑定的事件添加
return false;
39、js对象属性遍历方法
Object.keys(obj)
方法遍历、Object.values(obj)
- .返回一个数组
- 包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。
for(let key in obj)
循环遍历- 遍历对象自身(包含继承)的可枚举属性(不含Symbol属性).
Object.getOwnPropertyNames(obj)
遍历- 返回一个数组
- 含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)
Reflect.ownKeys(obj)
(Reflect
是一个内置的对象,联系到和Proxy
区别)ES6中的Proxy(Reflect)- 返回一个数组
- 包含对象自身的所有属性,不管是属性名是Symbol或字符串,也不管是否可枚举。
40、for…of , forin 和 forEach,map 的区别
方法 | 可遍历数据结构 | 语法 | tip |
---|---|---|---|
for...in | 数组、字符串、对象(一般就是用来遍历对象的) | for(let pro in obj) | 根据key遍历,遍历数组时获得的是数组的下标(但是输出顺序不固定) |
for...of | iterable可迭代对象(数组、Map,Set、字符串)普通对象不能 | for(let itemVal of arr) | 遍历数组时获得的是数组的值 |
forEach | 数组(迭代) | arr.forEach( (value,index,currentArr) => {} ) | 这个方法没有返回值,返回undefined。强制return的话只会跳出此次循环,不会终止整个循环。即它一定会对每一项迭代一遍,return false 也只是会结束本次迭代。如果要强制终止所有迭代,只能throw Error 。一般在数组中查找某一特定项,找到以后就没必要再继续查找了,这个时候用for 循环 就比较好。还有它无法保证遍历的顺序 |
map | 数组(映射) | arr.map( (value,index,currentArr) => { return val * 2; } ) | 返回一个新数组,新数组的内容是回调函数的返回值。可以用来克隆数组,原数组不变。 |
reduce | 数组(汇总) | arr.reduce( (value1,value2) => { return x + y} ) | 传入函数的参数一定是2个,累积计算,返回的是计算后的值。 |
filter | 数组(过滤) | arr.filter( (value,index,currentArr) => { return value % 2 !== 0;} ) | 把传入的函数依次作用于每个元素,然后根据返回值是 true 还是false决定保留还是丢弃该元素。不改变原数组。返回剩下匹配的元素 |
some | 数组 | 对数组中的每一项运行给定函数,至少有1项返回true,则返回 true | |
every | 数组 | 对数组中的每一项运行给定函数,如果该函数对每一项都返回 true,则返回 true; |
41、ES6 新特性
- 声明变量的关键字:
const
和let
(块级作用域)《let const var 区别》 - 变量的解构赋值:变量互换、从函数返回多个值(之前使用对象)
- 模板字符串:
Hello ${name},how are you ${time}
(字符串拼接) - 对象和数组新增了扩展运算符
spread
将数组转为用逗号分隔的参数序列、剩余运算符Rest
用于获取函数调用时传入的参数(返回一个数组)。spread和rest可以说是互逆(可用来将类数组转化为数组),es9后对象也可以使用拓展运算符 - 箭头函数:( => )在箭头函数内部,this并不会跟其他函数一样指向调用它的对象,而是继承上下文的this指向的对象。
- 函数可以设置默认参数值:
function printText(text = "hello world!")
- 使用
export
和import
实现模块化(项目中封装一些API 复用) 《ES6 模块与 CommonJS 模块的差异》 - 新增了
Set
(成员的值都是唯一)数组去重 、和Map
(提供了“值—值”的对应) 数据结构 - ES6 原生提供
Proxy
构造函数,用new Proxy(target, handler)
生成 Proxy 实例,通过拦截,对传进来的对象进行过滤和改写,提供了13种操作方法。vue3.0响应式原理 - ES6 新增了异步操作
Promise
、Generator
、async/await
ES6学习总结
42、前端调试方法
chorme控制台调试
- .Elements 功能标签页
- 实时编辑DOM节点
- 实时编辑CSS样式
- 打开盒子模型,调试各个属性参数
- 避免了要没修改一点都要运行一次,复杂繁琐
- .Console控制台:
- 显示报错信息
- 用于打印和输出相关的命令信息
- 运行JavaScript脚本等等功能
- 全局搜索(快捷键Ctrl+Shift+F)
- Sources js资源页面:这个页面内我们可以找到浏览器页面中的js 源文件,方便我们查看和调试
- Sources下面的左侧的Sinppets代码片段按钮(比控制台方便)
- 找源文件进行断点调试(常用按钮及快捷键)
- 三角按钮:直接到跳到下一个断点(F8)
- 右侧的第二个按钮:跳到下一步(逐步跨方法)(F10)
- 右侧的第三个按钮:跳入函数中去(F11)
- 右侧的第四个按钮:从执行的函数中跳出(shift+F11)
- 右侧的第五个按钮: 禁用所有的断点,不做任何调试(Ctrl+F8)
- 右侧的第六个按钮:程序运行时遇到异常时是否中断的开关
- 断点调试:
- 查看断点处,获取的数据的值
- Remove all breakpoints可以一次性删除所有断点。
- 事件监听断点:有助于我们快速找到某一个元素上绑定的事件。
- Network 网络请求标签页:可以看到所有的资源请求,包括网络请求,图片资源,html,css,js文件等请求,可以根据需求筛选请求项,一般多用于网络请求的查看和分析,分析后端接口是否正确传输,获取的数据是否准确,请求头,请求参数的查看
- performance:网页性能分析 《window.performance解读》
- memory:内存分析(内存泄露检测)
- application:该面板主要是记录网站加载的所有资源信息,包括存储数据(Local Storage、Session Storage、IndexedDB、Web SQL、Cookies)、缓存数据、字体、图片、脚本、样式表等。
- Security:通过该面板你可以去调试当前网页的安全和认证等问题并确保您已经在你的网站上正确地实现HTTPS。
- Audits:对当前网页进行网络利用情况、网页性能方面的诊断,并给出一些优化建议。比如列出所有没有用到的CSS文件等。
43、0.1+0.2 != 0.3的问题
- 是因为在进制转换、进阶运算的过程中出现精度损失。
- js是使用
number
数据类型来表示数字(包括整数、浮点数),使用64位的二进制表示一个数。 - 0.1和0.2转换成二进制后会无限循环,由于尾数位数有限制,需要截掉后面多余的这样在进制之间的转换中精度已经损失。
- 由于指数位数不相同,运算时需要对阶运算 这部分也可能产生精度损失。
- 最后结果转换成十进制之后就是 0.30000000000000004。
浏览器内核
-
Trident:这种浏览器内核是 IE 浏览器用的内核,因为在早期 IE 占有大量的市场份额,所以这种内核比较流行,以前有很多网页也是根据这个内核的标准来编写的,但是实际上这个内核对真正的网页标准支持不是很好。但是由于 IE 的高市场占有率,微软也很长时间没有更新 Trident 内核,就导致了 Trident 内核和 W3C 标准脱节。还有就是 Trident 内核的大量 Bug 等安全问题没有得到解决,加上一些专家学者公开自己认为 IE 浏览器不安全的观点,使很多用户开始转向其他浏览器。
-
Gecko:这是 Firefox 和 Flock 所采用的内核,这个内核的优点就是功能强大、丰富,可以支持很多复杂网页效果和浏览器扩展接口,但是代价是也显而易见就是要消耗很多的资源,比如内存。
-
Presto:Opera 曾经采用的就是 Presto 内核,Presto 内核被称为公认的浏览网页速度最快的内核,这得益于它在开发时的天生优势,在处理 JS 脚本等脚本语言时,会比其他的内核快3倍左右,缺点就是为了达到很快的速度而丢掉了一部分网页兼容性。
-
Webkit:Webkit 是 Safari 采用的内核,它的优点就是网页浏览速度较快,虽然不及 Presto 但是也胜于 Gecko 和 Trident,缺点是对于网页代码的容错性不高,也就是说对网页代码的兼容性较低,会使一些编写不标准的网页无法正确显示。WebKit 前身是 KDE 小组的 KHTML 引擎,可以说 WebKit 是 KHTML 的一个开源的分支。
-
Blink:谷歌在 Chromium Blog 上发表博客,称将与苹果的开源浏览器核心 Webkit 分道扬镳,在 Chromium 项目中研发 Blink 渲染引擎(即浏览器核心),内置于 Chrome 浏览器之中。其实 Blink 引擎就是 Webkit 的一个分支,就像 webkit 是KHTML 的分支一样。Blink 引擎现在是谷歌公司与 Opera Software 共同研发,上面提到过的,Opera 弃用了自己的 Presto 内核,加入 Google 阵营,跟随谷歌一起研发 Blink。