高频
var,let,const,死区?
- var的声明会被提升到全局作用域或函数作用域,但仍然在原地方赋值。
- let,const并不会声明提升,因此在声明之前的代码区域就是暂时性死区。
a.x = a = {n: 2}; ref从左到右,复制从右到左
call,apply,bind的区别?
- call方法第一个参数作为函数内this的指向对象,第二个参数往后都作为函数参数传递给函数。
- apply相对call而言仅参数上有不同,第二个参数为一个列表,这个列表中的元素作为函数参数传递给函数。
- bind相对call而言,仅绑定新对象并返回该函数,而不立即执行。
什么是promise?
Promise 是一种异步编程的解决方案,它是一个对象或构造函数,用于处理异步操作的结果。 Promise 有三种状态:Pending(进行中)、Resolved(已解决,又称 Fulfilled)和 Rejected(已拒绝)。 它通过 then 和 catch 方法来处理异步操作的结果,一旦状态改变,Promise 的状态就不会再变。 与传统的回调函数相比,Promise 避免了回调地狱的问题。
说一下原型和原型链?
原型是JavaScript对象的继承机制。JS所有对象都有一个私有属性proto指向另一个名为原型的对象, 这个对象就是创建该实例的构造函数内的原型属性prototype。
原型对象也有一个自己的原型,这样就构成了原型链,层层向上直到一个对象的原型为 null。
根据定义,null 没有原型,并作为这个原型链中的最后一个环节。 JavaScript 中,万物皆对象,对象分为普通对象和函数对象。 所有的函数都是函数对象(
typeof f === 'function'
),其他都是普通对象(typeof o === 'object'
)。
JS有多少种继承方式?
- 原型链继承,在构造函数外将子构造函数的原型属性prototype指向父类对象实例实现继承。 但子类新实例无法向父类构造函数传参,继承单一,所有子类新实例都会共享父类实例的属性。
child.__proto__ === Child.prototype === parent
parent.__proto__ === Parent.prototype
Parent.prototype.__proto__ === Object.prototype
Parent.__proto__ === Function.prototype
Child.__proto__ === Function.prototype
- 构造函数内继承,在子构造函数内调用父构造函数的call函数绑定子对象并初始化,解决了原型链继承方式的问题。 但是这样就完全没有用到原型,因此无法继承父类原型链上的属性。父类的属性方法也不会复用。
- 组合继承,组合原型链继承和构造函数内继承合在一起。这样就能同时解决了原型链继承和构造函数继承的问题。 但每次都会调用两遍父类构造函数,子类原型上也有一份多余的父类实例属性。
- 寄生式继承,每次构造时都拷贝一份原型对象,并给对象增加子类方法和属性。但这样是浅拷贝,因此引用类属性将被共享。
- 寄生式组合式继承,将组合寄生式继承和组合式继承合在一起。 通过借用构造函数来继承属性, 在原型上添加共用的方法, 通过寄生式实现继承.
function Parent(name, age){
this.name = name;
this.age = age;
}
Parent.prototype.getName = function(){
return this.name;
}
function Child(name){
Parent.call(this, name, 12);
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
let child = New Child('mike');
console.log(child.getName(), child.age); // mike 12
每个对象都有constructor(构造函数)属性,这个属性指向 prototype 属性所在的函数,
a.constructor === A.prototype.constructor === b
。
普通继承的关系
function F(){}
var f = new F();
// 构造器
F.__proto__ === Function.prototype; // true
F.prototype.constructor === F; // true
F.__proto__.constructor === Function.prototype.constructor === F // true
F.prototype.__proto__ === Object.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
// 实例
f.__proto__ === F.prototype; // true
Object.prototype.__proto__ === null; // true
讲一讲js的this指针?
- 全局的this指针指向windows。在严格模式下,全局的this指针是undefined。
- 在函数中,this 表示全局对象。在严格模式下,this是undefined。
- 当一个方法被调用时,this被绑定到这个对象上。
- 如果一个函数当构造函数用,函数中的this会被绑定到这个新对象上。
- 事件的this指针指向元素本身。
- call/apply/bind,this指针会绑定指定的对象。
- 箭头函数中的this是指向箭头函数外的this,即箭头函数没有自己的this指针。
闭包是什么?
闭包是指一个函数可以访问和使用定义在函数外部的变量,这些变量会始终保持在内存中,能提供很好的封装和抽象。 但是也可能会导致内存泄漏。
事件循环机制?
JS是一门单线程的语言,事件循环是JS的异步执行机制。事件循环的工作流程是
- 首先,检查执行栈,看看是否有同步任务需要执行。
- 如果执行栈为空,那么就检查任务队列。
- 如果任务队列中有待处理的任务,那么就将它移出队列并放入调用堆栈并执行这个任务。
任务队列分为宏任务队列和微任务队列。在一个事件循环迭代中,首先执行一个宏任务,然后执行所有的微任务。当所有的微任务完成后,再执行下一个宏任务。
宏任务包括如setTimeout,setInterval,setImmediate,I/O,UI rendering等(setTimeout里的回调函数算宏任务), 而微任务包括如Promise,MutationObserver等(注意promise内的回调函数会当作同步代码立即执行,then/catch里才是微任务) (微任务中如果新建了一个微任务,那么将继续执行微任务而不去执行宏任务)。
捕获与冒泡?
在 HTML 中,当事件被触发时,事件会经过三个阶段:捕获阶段、目标阶段和冒泡阶段。
- 捕获阶段:从 document 对象开始逐级向下传递直到事件源元素。事件处理函数会按照由父元素到子元素的顺序被依次执行。
- 目标阶段:当事件传递到事件源元素时,就进入了目标阶段。在目标阶段可以通过event.target获取到触发事件的具体元素。
- 冒泡阶段:从事件源元素开始逐级向上传递直到 document 对象。事件处理函数会按照由子元素到父元素的顺序被依次执行。
可以通过 addEventListener() 函数的第三个参数(选项)设置 capture 选项来监听事件的捕获阶段,否则监听将在冒泡阶段触发。 事件传递过程中,如果事件处理函数调用了 event.stopPropagation() 方法,当侦听的事件是捕获时,阻断的就是捕获过程,当侦听的事件是冒泡时,阻断的就是冒泡过程。 捕获stop会阻断冒泡。
typeof,instantof是什么?
- typeof是一个运算符,返回值是一个字符串,用来说明变量的数据类型, 可以用此来判断number, string, object, boolean, function, undefined, symbol 这七种类型。 但是对于对象、数组、null 返回的值是 object。可以通过Array.isArray(),instanceof,Object.prototype.toString().call()
- instanceof运算符用于指示一个变量是否属于某个对象的实例。返回值为布尔值。instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。
讲一讲es5/6/7/8/9新特点?
- ECMAScript 5 也称为 ES5 和 ECMAScript 2009。
"use strict"
指令。例如,使用严格模式,不能使用未声明的变量String.trim()/charAt()
删除字符串两端的空白字符Array.isArray()/forEach()/map()/fliter()/indexOf()/reduce()
JSON.parse()/stringify()
Date.now()
Object.key()/defineProperty()/getPrototypeOf()
等- 属性 Getter 和 Setter
- ECMAScript 6,也被称为ES6和ES2015,是JavaScript的第六个版本
- let 和 const 以及块级作用域,在ES6之前,JavaScript只有全局作用域和函数作用域,没有块级作用域。
- 模板字符串,
String.startWith()/endWith()
- 箭头函数
- 函数参数的默认值,剩余参数,解构赋值,展开运算符
- set和map数据结构
- 遍历器与for…of循环
Object.keys()/values()/entries()/assign()
- 对象字面量的增强:属性和方法的简洁表示法,方括号表示法
- 全局作用域中的 this 指向,在严格模式下,全局作用域中的 this 的值是 undefined。在非严格模式下,它会指向全局对象(浏览器中是 window 对象,Node.js 中是 global 对象)。
Array.includes()/find()/from()/fill()
- ES2016(ES7)
- 幂运算符** 但一般也没有什么需求会用到幂乘
Array.prototype.includes()
,第二个参数是从第几个下标开始搜,默认0
- ES2017 ES8
Object.values
,Object.entries
- 结尾允许逗号
- async异步函数
- ES2018 ES9
- 异步循环,同步循环中调用能引起等待的异步函数,是不会达到预期目的,循环本身依旧保持同步,并在内部异步函数之前完成。因此新增异步循环
for await(let i of array)
- 异步的finally
- 对象的展开运算符,这项特性在ES6中已经引入,但是仅限于数组。
- 异步循环,同步循环中调用能引起等待的异步函数,是不会达到预期目的,循环本身依旧保持同步,并在内部异步函数之前完成。因此新增异步循环
- ES2019 ES10
- catch的参数e可以省略掉
Object.fromEntries()
方便地将键值对列表(例如 Map、数组(符合键值对的)等)转换为一个对象。Array.flat()
能将高维数组降一维。
- ES 2020 (ES11)
- 可选链操作符
- BigInt 它是JavaScript的第7个原始类型,可安全地进行大数整型计算。 只需要在数字后面加上 n 即可。但不能将 BigInt与Number算术计算。
for in
, for of
区别?
for in
循环返回的值都是对象的键值名(数组即下标),遍历顺序有可能按照实际数组的内部顺序, 使用for… in会遍历数组或对象所有的可枚举属性,包括继承属性和原型。所以不适合遍历数组,更适合遍历对象。- for… of 循环用来获取一对键值对中的值,但for…of循环内部调用的是数据结构的迭代器。因此不能遍历对象,因为普通对象没有迭代器, ,可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、Generator 对象,以及字符串。 相对于forEach而言可以与 break、continue和return 配合使用,可以随时退出循环。
在ECMAScript规范中定义了 「数字属性应该按照索引值⼤⼩升序排列,字符串属性根据创建时的顺序升序排列。」在这⾥我们把对象中的数字属性称为 「排序属性」,在V8中被称为 elements,字符串属性就被称为 「常规属性」, 在V8中被称为 properties。
JS 的array的函数中,有哪些是直接修改数组本体(变异方法)?
- 改变原数组的方法:
pop()
,push()
,shift()
,unshift()
,sort()
,reverse()
,splice()
- 不改变原数组的方法:
slice()
,concat()
,map()
,forEach()
赋值/浅拷贝/深拷贝的区别?
- 赋值是拷贝的对象指针,整个对象都是共用的。
- 浅拷贝是拷贝一层,对象的内容仍是共用的,
Object.assign()
,拓展运算符都是浅拷贝。 - 深拷贝是递归拷贝深层次,
JSON.stringify()
是深拷贝,但是会忽略undefined、symbol和函数。
知道JS的set和map的用法吗?
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set本身是一个构造函数,用来生成 Set 数据结构。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}// 2 3 5 4
// 去除数组的重复成员
[...new Set(array)]
// Array.from方法可以将 Set 结构转为数组。
const array = Array.from(s);
//去除字符串里面的重复字符。
[...new Set('ababbc')].join('')
// "abc"
另外,Set还有size()
,delete(v)
,has(v)
,clear()
等方法。
Map类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。 如果你需要“键值对”的数据结构,Map 比 Object 更合适。
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // false
CommonJS 和 ES6模块的区别是什么?
- Node.js 的模块系统采用 CommonJS 规范,它并不是 JavaScript 语言规范的正式组成部分。 使用require()加载和module.exports输出,代码运行阶段同步加载JS文件。
- 前端的模块系统则采用ES6模块规范,这是 JavaScript 语言规范的正式组成部分。使用import加载和export输出, 在静态解析阶段分析依赖关系,输出符号引用。ES6模块JS文件相当于自带defer属性,异步加载JS文件,不阻塞DOM构建。
不同点:
- ES6输入的模块变量是一个引用,变量是只读的,对它进行重新赋值会报错。
- CommonJS输入的模块变量是一个浅拷贝,如果访问原模块内的实时数据,通过函数返回内部值仍然是可以的。。
共同点是
- 无论在前端还是在后端,ES6规范和CommonJS也是可以在一个项目中同时使用,甚至相互混用。
- 二者对于同一模块多次加载都只会执行一次模块内代码,即首次加载执行,后面加载模块不执行其内部代码。
因此export命令后面只能跟着声明式语句,而不能跟表达式(如变量名,字面量)。因为变量只有在声明时,才会产生一个变量引用的符号。
另外,ES6模块的export 中 不是一个对象简写形式,更不是一个对象,而是export 语法组成部分,用作收集符号用。
另外,CommonJS的module.exports不是模块内部的变量,而是外部传入模块的变量,所以一旦模块内部代码对于exports变量做了修改,其实就是对于外部该变量做了修改, 因此模块代码未执行完,模块的输出module.exports也是有值的,因为这是外部值。
AMD/CMD 规范?
AMD/CMD都是浏览器的JS模块加载机制。 CMD 推崇依赖就近(用的时候再声明引用的依赖),AMD 推崇依赖前置(先声明引用的依赖)。
- AMD规范,是异步模块加载机制,推崇依赖前置。核心是预加载,先对依赖的全部文件进行加载,加载完了再进行处理。解决的是JS加载引起页面卡顿的问题。require/define。
- CMD规范,是同步模块加载机制,推崇依赖就近。按不同的先后依赖关系对 JavaScript 等文件的进行加载工作, 确保各个JS文件的先后加载顺序,确保避免了以前因某些原因某个文件加载慢而导致其它加载快的文件需要依赖其某些功能而出现某函数或某变量找不到的问题。require/define。
JS的隐式转换?
在使用不同类型的值进行操作时,JavaScript会自动进行类型转换,这称为隐式转换。
- 我们在对各种非Number类型运用数学运算符(- * /)时,会先将非Number类型转换为Number类型。
- 我们在对各种非Number类型运用数学运算符(+)时: (以下 3 点,优先级从高到低)
- 当一侧为String类型,会将另一侧转换为字符串类型并进行字符串拼接。
- 当一侧为Number类型,另一侧为非字符串的其他原始类型,则将原始类型转换为Number类型。
- 当一侧为Number类型,另一侧为引用类型,将引用类型和Number类型转换成字符串后拼接。
- 当我们使用逻辑语句(例如if while for)时,条件值将转为Boolean值,只有 null undefined '' NaN 0 false 这几个是 false,其他的情况都是 true,比如 , []。。
- 当我们使用
==
时:- NaN和其他任何类型比较永远返回false(包括和他自己)。
- Boolean 和其他任何类型比较,Boolean 首先被转换为 Number 类型。
- String和Number比较,先将String转换为Number类型。
- null == undefined比较结果是true,除此之外,null、undefined和其他任何结果的比较值都为false。
- 原始类型和引用类型做比较时,引用类型会依照ToPrimitive规则转换为原始类型。
中级
null是原始类型,但为什么typeof null的结果是object?
造成这个结果的原因是null的内存地址是以000开头,而js会将000开头的内存地址视为object。
通过isNull()
来判断一个值是不是null类型,但值得注意的是isNaN()
会进行隐式转换。
typeof 无法精确的检测null、Object、Array。
低频
为什么在JS中0.1+0.2!=0.3?以及IEE 754标准
JavaScript使用Number类型表示数字(整数和浮点数),遵循 IEEE 754 标准 通过64位来表示一个数字。
首先,计算机无法直接对十进制的数字进行运算,这是硬件物理特性已经决定的。这样运算就分成了两个部分: 先按照IEEE 754转成相应的二进制,然后按照二进制运算。
回到0.1+0.2的例子上,首先转成二进制后,二进制数字是无限循环的,但是由于IEEE 754尾数位数限制, 需要将后面多余的位截掉,这样在进制之间的转换中精度已经损失。
由于指数位数不相同,运算时需要对阶运算 这部分也可能产生精度损失,两步的精度损失最后的结果转换成十进制之后就是0.30000000000000004。
只要是遵循遵循 IEEE 754 标准的语言都会有这个问题。
JS的垃圾回收机制?
有两种垃圾回收策略:
- 标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
- 引用计数:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 0),对象将被垃圾回收机制回收。
标记清除的缺点:
- 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
- 分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。
引用计数的缺点:
- 需要一个计数器,所占内存空间大,因为我们也不知道被引用数量的上限。
- 解决不了循环引用导致的无法回收问题(不可达孤岛)。
垃圾回收是一个规模庞大的工作,尤其在代码量非常大的时候,频繁执行垃圾回收算法会明显拖累程序的执行。JavaScript算法在垃圾回收上做了很多优化,从而在保证回收工作正常执行的前提下,保证程序能够高效的执行。
- 分代回收 JavaScript把局部/全局对象分开管理,对于快速创建、使用并丢弃的局部变量,垃圾回收器会频繁的扫描,保证这些变量在失去作用后迅速被清理。 而对于哪些长久把持内存的变量,降低检查它们的频率,从而节约一定的开销。
- 增量收集 增量式的思想在性能优化上非常常见,同样可以用于垃圾回收。 在变量数目非常大时,一次性遍历所有变量并颁发优秀员工标记显然非常耗时,导致程序在执行过程中存在卡顿。 所以,引擎会把垃圾回收工作分成多个子任务,并在程序执行的过程中逐步执行每个小任务,这样就会造成一定的回收延迟,但通常不会造成明显的程序卡顿。
- 空闲收集 CPU即使是在复杂的程序中也不是一直都有工作的,这主要是因为CPU工作的速度非常快,外围IO往往慢上几个数量级, 所以在CPU空闲的时候安排垃圾回收策略是一种非常有效的性能优化手段,而且基本不会对程序本身造成不良影响。 这种策略就类似于系统的空闲时间升级一样,用户根本察觉不到后台的执行。