原型和原型链
- 原型
- 每个JS函数都有一个
prototype属性,改属性指向的对象被称为原型对象。同时这个对象是这个函数的实例的原型对象。所有由这个函数构造出来的实例都有一个__proto__属性指向原型对象,在原型对象上,有一个属性constructor指向实例的构造函数
- 每个JS函数都有一个
- 原型链
- 定义在原型对象上的所有属性和方法都可以被实例共享。多个原型组成的就是一条原型链,在JS中,访问一个对象中的属性和方法,首先在对象自身中查找,如果找到则返回,否则去这个对象的原型中查找,如果没找到,就去原型的原型中查找,一直找到
object的原型为止。如果没有找到,则返回undefined。 注:object实例的原型没有原型对象,为null。
- 定义在原型对象上的所有属性和方法都可以被实例共享。多个原型组成的就是一条原型链,在JS中,访问一个对象中的属性和方法,首先在对象自身中查找,如果找到则返回,否则去这个对象的原型中查找,如果没找到,就去原型的原型中查找,一直找到
数据类型、简单数据类型和复杂数据类型的区别
-
简单数据类型:ES5中有Number,String,Null,Undefined,Boolean,ES6新增了symbol(表示 独一无二的值,是一种类似于字符串的数据类型), BigInt(安全存储,操作大整数)
-
复杂数据类型:object (其中包括 function, array 等)
-
简单数据类型的值直接保存在
栈中,而复杂数据类型的值保存在堆中,栈中保存的只是复杂数据类型的堆内存地址。 -
NaNNaN是 Number 中的一种,非Number- 用
isNaN()检测是否是非数值型 - js 规定的 NaN 不等于 NaN (
NaN==NaN false)
-
例子
let a = [1,2,3] let d = {} function send(c,d){ c = [] d.b = 2 d = {a:1} } send(a,d) console.log(a) // [ 1, 2, 3 ] console.log(d) // { b: 2 }JS函数传参都是传值(无论是基础类型还是引用类型),可以看这篇:JS函数传参。
分析: a是数组,b是对象,都是引用类型,传的都是值(引用的地址值)。假设a的地址是0x001,d的地址是0x002,函数 send(c,d) 传入参数后,c = [] 开创了一个新的对象,假设c此时的地址是0x003,然后 d.b = 2,此时 d 的地址是 d 的地址,是0x002。但注意 d = {a:1} 时 d 被赋值了新的对象,是新的地址了,假设 d 新地址为0x004。console.log(a) 输出的是 a 的地址0x001中的值,是[ 1, 2, 3 ],console.log(b)输出的是 b 的地址0x002中的值,是{ b: 2 }
null和undefined
null表示“没有对象”,即该处不该有值- 作为函数的参数,表示函数的参数不是对象
- 作为原型链的终点
undefined“缺少值”,此处应该有一个值,但没有定义- 变量声明未赋值
- 调用函数时,参数未提供
- 对象没有赋值的属性
- 函数没有返回值
在if语句中,null和undefined被自动转成false;
Number(null)=0; Number(undefined)=NaN;
typeof(null)=object; typeof(undefined)=undefined;
判断JS数据类型的方法
typeof- 特殊: typeof(null)=object ; 引用类型均返回object,除typeof(function)-->function
A instanceof B- instanceof 运算符 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
- A是否是B的实例,检测的是原型;只能判断两对象是否属于实例关系,不能判断对象实例具体属于哪种;例如:
let arr = []; arr instanceof Array; // true arr instanceof Object; // true 原型链查找:[]---Array.prototype---Object.prototype---null
Object原型上的toString方法- Object.prototype.toString()通过apply/call绑定调用
console.log(Object.prototype.toString.call([])); // "[Object Array]" 字符串 console.log(Object.prototype.toString.call(null)); // "[object Null]" console.log(Object.prototype.toString.call(undefined)); // "[object Undefined]"
- Object.prototype.toString()通过apply/call绑定调用
===严格相等
判断数组、数组去重
判断数组的方法
Array.isArray()用于确定传递的值是否是一个数组,返回一个布尔值Object.prototype.toString.call(a) === '[object Array]';
数组去重
- 双重 for 循环,splice 去重
- 利用 indexOf / includes 去重
- 利用 filter 滤掉重复元素
- 利用 sort 排序后,遍历比较相邻元素来去重
- 利用 ES6 中的 Set 去重(原理是通过两个函数__hash__和__eq__结合实现的)
- 1、当两个变量的哈希值不相同时,就认为这两个变量是不同的
- 2、当两个变量哈希值一样时,调用__eq__方法,当返回值为True时认为这两个变量是同一个,应该去除一个。返回FALSE时,不去重
伪数组和数组的区别
- 数组
- 数组具有一个最基本的特征:索引
- 数组有Array.prototype,属性继承
- 真实数组长度是可变的
- 伪数组
- 具有length 属性,其它属性(索引)为非负整数(对象中的索引会被当做字符串来处理,这里你可以当做是个非负整数串来理解)
- 不具有数组所具有的一些方法
- 伪数组长度是不可变的 常见的伪数组有:
- 函数内部的 arguments
- DOM 对象列表(比如通过 document.getElementsByTags 得到的列表)
- jQuery 对象(比如 $("div") )
伪数组是一个 Object,而真实的数组是一个 Array。 可以看这篇:JavaScript伪数组和数组的使用与区别
闭包
说一下你对闭包的理解?闭包是什么?
-
闭包就是有权访问另一个函数作用域变量的函数,形成闭包的原因是存在对上级作用域变量的引用.
var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6 // 手写一个简单的闭包 -
闭包底层原理:JS代码要经过浏览器进行预编译后才能真正被执行,js代码运行需要一个运行环境,那这个环境就是
执行上下文。 -
执行上下文分三种:
- 全局执行上下文: 代码开始执行时首先进入的环境。
- 函数执行上下文:函数调用时,会开始执行函数中的代码。
- eval执行上下文:不建议使用,可忽略。
-
执行上下文的周期:
-
创建阶段:创建词法环境 -- 生成变量对象 -- 建立作用域链 -- 确定this指向
-
执行阶段:进行变量赋值 -- 函数引用及执行代码
-
闭包的作用?
- 使用闭包可以保护函数的私有变量不受外部干扰
- 也可以将上级作用域的引用保存下来,实现方法或属性的私有化
用闭包有什么弊端?
- 容易导致内存泄漏,因为存在其他作用域的引用,过渡使用闭包会使内存占用过多
经典使用场景
for循环的事件赋值引用:使用闭包来改善;使用let来改善
怎么检查内存泄漏
performance面板和memory面板可以找到泄露的现象和位置
可以看一下这篇:闭包
垃圾回收机制有了解吗?
当我们创建一个基本类型、对象或者函数时,引擎会自动分配内存。JS的引用数据类型是保存在堆内存中,栈内存中保存的是对堆内存地址的引用。当引用关系没有了,就需要被清理(回收),释放内存。 最常见的垃圾回收策略:标记清除算法 和 引用计数算法。
- 引用计数算法
- 策略就是跟踪记录每个变量值被使用的次数,判断该对象的引用数,引用数
为0就回收,引用数大于0就不回收 - 好处:当引用数为
0时就可以立即回收 - 缺点:需要计数器来每隔一段时间进行一次计数,计数器占位置,且会存在循环引用无法回收的问题
- 策略就是跟踪记录每个变量值被使用的次数,判断该对象的引用数,引用数
- 标记清除算法
- 将可达的对象标记起来,不可达的对象当成垃圾回收。
- 好处:实现简单,标记和未标记两种情况,用二进制位(0和1)就能做标记
- 缺点:清除后剩余对象内存位置不连续,内存分配问题
V8的垃圾回收算法
-
分代回收:
- V8将堆分为两个空间,一个叫新生代,存放存活周期短的对象;一个叫老生代,存放存活周期长的对象
Scavenge算法:主要负责新生代的垃圾回收Mark-Sweep && Mark-Compact算法:主要负责老生代的垃圾回收
-
全停顿(Stop-The-World)
- JS代码和垃圾回收都会用到JS引擎,如果两者同时进行,发生冲突,
垃圾回收优先于代码执行,会先停止代码的执行,等到垃圾回收完毕,再执行JS代码。这个过程,称为全停顿
- JS代码和垃圾回收都会用到JS引擎,如果两者同时进行,发生冲突,
-
Orinoco优化
- 增量标记:针对标记阶段,当垃圾达到一定数量时,增量标记就会开启,垃圾标记一点,JS代码运行一段,从而提高效率
- 惰性标记:针对清除阶段,在增量标记后,要进行清理时,垃圾回收器发现了其实就算是不清理,剩余的空间也足以让JS代码跑起来,所以就
延迟了清理,让JS代码先执行,或者只清理部分垃圾,而不清理全部。这个优化就叫做惰性清理。缺点就是不及时清理,可能造成对象引用改变,标记错误的现象。 - 并行回收:垃圾回收器在主线程上执行的过程中,开启多个辅助线程,同时执行同样的回收工作,但还是会阻塞主线程
- 并发回收:在垃圾回收的同时不需要将主线程挂起,两者可以同时进行
更多可以看这篇:垃圾回收
for..in 和 for..of 、forEach 和 map
for..in 和 for..of
for..in遍历的是数组的索引(即键名),for..of遍历的是数组元素值(键值)for..in会遍历数组所有的可枚举属性,包括原型,更适合遍历对象for..of适用遍历数/数组对象/字符串/map/set等 拥有迭代器对象的集合。但是不能遍历对象,因为没有迭代器对象.与forEach()不同的是,它可以正确响应break、continue和return语句let arr=[{name:'张三'},{name:'李四'}]; let obj={name:'张三'}; for (let i in arr){ // for..in遍历的是数组的索引(即键名) console.log(i) // 0 1 } for (let i of arr){ // for..of遍历的是数组元素值 console.log(i) // { name: '张三' } { name: '李四' } } for (let i in obj){ console.log(i) // name -- 对象的键名 // for in会遍历数组所有的可枚举属性,包括原型,更适合遍历对象 } for (let i of obj){ // **for...of不能对象用** console.log(i) // TypeError: obj is not iterable }- 获取对象上属性的方法:
- for in 可以遍历到myObject对象的原型方法method,如果不想遍历原型方法和属性的话,可以在循环内部用以下两种方式判断一下
- hasOwnPropery方法可以判断某属性是否是该对象的实例属性,返回一个数组
for (var key in myObject) { if(myObject.hasOwnProperty(key)){ console.log(key); } } - 同样可以通过 ES5的Object.keys(myObject) 获取对象的实例属性组成的数组,不包括原型方法和属性
forEach 和 map
-
foreEach() 方法: 针对每一个元素执行提供的函数
- 不会返回执行结果,结果是undefined,会对每个元素执行所提供的的函数,但不一定改变原数组,除非直接对数组元素进行操作
let arr =[1,2,3,4]; let arr1 = arr.forEach((item) => { return item * item; // 针对每一个元素执行提供的函数,没有返回值,不一定改变数组的值 }); console.log(arr) // [ 1, 2, 3, 4 ] console.log(arr1) // undefined --- 没有返回值 arr.forEach((value, key) => { return arr[key] = value * value; // 这里是直接对数组元素的值进行了修改 }); console.log(arr) // [ 1, 4, 9, 16 ] // 数组元素被改变
- 不会返回执行结果,结果是undefined,会对每个元素执行所提供的的函数,但不一定改变原数组,除非直接对数组元素进行操作
-
map() 方法: 创建一个新的数组,用来储存对数组每个元素执行函数得到的结果
let arr =[1,2,3,4]; let res = arr.map((item) => { // 返回一个新数组 return item * item; }); console.log(arr) // [ 1, 2, 3, 4 ] -- 原数组不变 console.log(res) // [ 1, 4, 9, 16 ]
事件捕获、事件冒泡、事件代理
-
事件捕获
- 当一个事件触发后, 从
Window对象触发, 不断经过下级节点, 直到目标节点。在事件到达目标节点之前的过程就是捕获阶段。所有经过的节点, 都会触发对应的事件
- 当一个事件触发后, 从
-
事件冒泡
- 当事件到达目标节点后,会沿着捕获阶段的路线原路返回。同样,所有经过的节点,都会触发对应的事件
- 当事件到达目标节点后,会沿着捕获阶段的路线原路返回。同样,所有经过的节点,都会触发对应的事件
-
事件委托(代理)的作用
-
- 支持为同一个DOM元素注册多个同类型事件
-
- 可将事件分成事件捕获和事件冒泡机制
-
-
用addEventListener(type,listener,useCapture)实现事件监听:
type: 必须,String类型,事件类型listener: 必须,函数体或者JS方法useCapture: 可选,boolean类型。指定事件是否发生在捕获阶段。默认为false,事件发生在冒泡阶段
-
event.target 和 event.currentTarget 的区别
-
event.target指向引起触发事件的元素 -
event.currentTarget则是当前触发事件的元素 -
只有被点击的那个目标元素的
event.target才会等于event.currentTarget捕获阶段 冒泡阶段 target:d & currentTarget:a target:d & currentTarget:d target:d & currentTarget:b target:d & currentTarget:c target:d & currentTarget:c target:d & currentTarget:b target:d & currentTarget:d target:d & currentTarget:a
-
-
阻止事件冒泡和阻止默认事件的区别
-
event.stopPropagation()方法:只阻止事件往上冒泡,不阻止事件本身,默认事件任然会执行,例如你点击一个连接,这个连接仍然会打开。
-
event.preventDefault()方法:阻止默认事件的方法,调用此方法是,连接不会被打开,但是会发生冒泡,冒泡会传递到上一层的父元素
-
return false: 即阻止事件冒泡也会阻止默认事件,连接不会被打开,事件也不会传递到上一层
不是所有的事件都能冒泡。以下事件不冒泡:blur、focus、load、unload。
-
JS获取DOM节点的方法
- 通过 id 获取
document.getElementById('id名'): 返回的是对应Id的DOM对象
- 通过 class 获取:
document.getElementsByClassName('class名'):返回的是对应class的DOM节点对象集合,是一个伪数组,注意是所有节点
- 通过 标签名 获取
document.getElementsByTagName('标签名'):返回的是对应标签名的DOM节点对象集合,是一个伪数组,注意是所有节点
- 通过 css选择器 获取
document.querySelector('..'): 参数:css选择器,#id,.class,tag等,返回的是对应css选择器对应的第一个标签- document.querySelectorAll():参数同上,返回的是对应css选择器对应的DOM属性节点对象集合,是一个伪数组,注意是元素节点
JSON.stringify()转换
var a = function(){}
var b = [1,a]
console.log(JSON.stringify(a)) // undefined
console.log(JSON.stringify({a})) // {}
console.log(JSON.stringify(b)) // [1,null]
JSON.stringify(a) 是 undefined , 当一个函数当做对象去用的时候,如果是字符串,会调用该函数的toString()方法,该方法没有返回值,那么这个函数就是 undefined ; 同理如果加上 {fn} 的话,返回 {}。
JSON.stringify()转换详细看这里:MDN---JSON.stringify()
Object.defineProperty的属性
Object.defineProperty(obj:对象, prop:key, descriptor:属性和方法)
let obj = {}
Object.defineProperty(obj,'a',{
value:undefined //1.就是value值
configurable: true //2.是否允许配置对象,删除属性
writable:true //3.是否允许赋值,修改对象
enumerable:true //4.是否允许枚举对象
// get、set设置时不能设置1.3,他们代替了二者,是互斥的
get() {} //获取的return的值,需要return 否则会回去到 undefined
set(newVal) {} //修改的时候执行 newVal就是最新的值,可以修改数据、执行函数、dom操作等一些方法
})
可以看这篇:详解JavaScript之神奇的Object.defineProperty
高阶函数
高阶函数是一个可以接收函数作为参数,甚至返回一个函数的函数。 它就像常规函数一样,只是多了接收和返回其他函数的附加能力,即参数和输出。常用的高阶函数:
map: 对输入数组中每个元素调用回调函数来创建一个新数组filter(): 会创建一个新数组,其中包含所有通过回调函数测试的元素reduce: 对调用数组的每个元素执行回调函数,最后生成一个单一的值并返回- 函数柯里化--高阶函数