Javascript
1、基本的数据类型介绍,及值类型和引用类型的理解
基本数据类型:String、Number、Boolean、Undefined、Null、Symbol、BigInt、Object
ES6新增:
- Symbol 代表独一无二的值,最大的用法是用来定义对象的唯一属性名。
- BigInt 可以表示任意大小的整数。
值类型、引用类型:
值类型存储在栈中的简单数据段,占据空间小,大小固定。栈中数据的存取方式是先进后出,栈内存由编译器自动释放。
let a = 100;
let b = a;
a = 200;
console.log(b); // 100
引用数据类型存储在堆中,占据空间大,大小不固定。如果存储在栈中会影响程序运行性能。堆是一个优先队列,按优先级进行排序。堆内存一般由开发者手动释放,或者程序结束时可能由垃圾回收机制回收。
2、数据类型的检测方式:
typeof、instanceof、constructor、Object.propertyof.toString.call()
typeof:用来判断基本数据类型,数组、对象、null 都会被判断为 'object'。
typeof xx === 'xx类型'
typeof undefined === 'undefined'
typeof null === 'object'
typeof 1 === 'number' // true
typeof [] === 'object' // true
typeof {} === 'object' // true
typeof function(){} === 'function' // true
array:因为 Array 是 Object 的子集,
null:
- null 是由于JavaScript 的历史原因导致的,第一版本中所有值都存储在一个32位的单元中,其中每个单元包含一个小的类型标签以及当前要存储的真实数据。
- 数据类型分为5类【object、String、boolean、int、drouble】,如果最低位为 1 则数据类型就为1;如果为 0 为了区分其他4类前面加了两个比特长度,最终 object 的数据类型表示位 000。
- 而 NULL 对应的机器码指针的值全是0,所以 null 也被认为是 'object' 类型。
instanceof: 用来判断引用数据类型。基本数据类型都为 false。
xx intanceof xx类型
1 instanceof Number // false
[] instanceof Array // true
{} instanceof Object // true
原理:判断一个对象在其原型链上是否存在一个构造函数的原型。
function myIntanceOf(left, right) {
let proto = Object.getPrototypeOf(left); // 获取原型
let prototype = right.prototype;
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto)
}
}
constructor:不能判断undefined和null,并且使用它是不安全的,因为contructor的指向是可以改变的
(9).constructor === Number
true.constructor === Boolean
obj.constructor === Object
...
Object.prototype.toString.call():调用Object 的原型方法
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(1); // '[object Number]'
Object.prototype.toString.call({}) // '[object Object]'
Object.prototype.toString.call([]) // '[object Array]'
Object.prototype.toString.call(null) // '[object Null]'
Object.prototype.toString.call(undefined) // '[object Undefined]'
obj.toString 和 Object.prototype.toString.call(obj) 结果不一致:
- toString() 是 Object 原型的方法,Array、function 等重写了 toString 方法。
- 不同的对象调用 toString 方法时,根据原型链的知识,调用的是对应重新之后的 toString 方法
- 而不是 Object 原型 toString 方法。
3、判断数组的方式
[] instanceof Array
[].constructor === 'Array'
Object.prototype.toString.call([]).slice(8,-1) === 'Array'
Array.isArray([])
[].__proto__ === Array.prototype // 判断对象的原型链中是否有 数组的原型
4、null、undefined 区别,安全获取 undefined 值
null:表示空对象,主要用于给可能返回对象类型的变量作为初始化。
undefined:表示未定义,一般申明了却未定义,会返回 undefined。
undefined 在JavaScript 中不是一个保留字,可以使用 undefined 作为变量使用,但是这样会影响 undefined 值的判断 可以通过 void 0 获得安全的 undefined
5、0.1 + 0.2 != 0.3 为啥,如何相等
0.1 和 0.2 转为二进制都为无限小数,而 javascript 双精度精确到 53 位有效数字,所以进行计算后值不等于 0.3
如何想等:
( 0.1 * 10 + 0.2 * 10 ) / 10
利用ES6提供的机器精度,小于机器精度则认为相等:
function epsilon(arg1, age2) {
return Math.abs(age1 - age2) < Number.EPSILON
}
epsilon(0.1 + 0.2, 0.3) // true
6、typeof NaN 结果是什么
- typeof NaN === 'number' // true
- NaN:表示不是一个数字,是进行指出数字类型错误的情况的返回。
- 是一个特殊值,及 NaN !== NaN
7、 isNaN 和 Number.isNaN 的区别
- 用来判断是否为 NaN
- isNaN 进行判断不为 Number 类型时,会先将参数进行转换为 numbr ,再进行判断
- Number.isNaN ,判断不会 Number 类型时,会直接返回false,用于严格判断是否为 NaN
isNaN("hhh") // true
Number.isNaN("hhh") // false
8、Object.is 与 '==='、‘==’ 区别
- == 判断不同类型的属性时,会先进行强制类型转换,再进行判断。
- === 判断不同数据类型的属性,会直接返回 false。
- Object.is Es6 语法,与 === 一致,优化了以下两种情况:两个 NaN 想等,+0 与 -0 不等。
Object.is(NaN, NaN) // true
Object.is(-0, +0); // false
9、什么是 JavaScript 包装类型
JavaScript 中,基本类型是没有数据和方法的,在调用基本类型的属性、方法时,会先将基本属性隐式的转换成对象,再进行调用。就是 JavaScript 包装类型。
直接调用:先将 string 转换成 String('abc'),再访问 length 属性。
'abc'.length
也可以使用 Object() 显示的将基本属性转换为包装类型
Object('abc')
也可以使用 valueOf() 将包装类型再转换为基本类型
const b = Object('abc')
b.valueOf()
10、JavaScript 如何隐式的进行类型转换
通过 toPrimitive 方法,将值转换为基本数据类型。 如果值为基本数据类型则返回本身, 如果为对象则如下:
toPrimitive(obj,type)
- type的值为 number 或者 string,默认 type 为 number
- type:number:会先调用 valueOf 方法,如果为原始值则返回;否则调用 obj 的 toString 方法 如果为原始值返回;否则 抛出 TypeError 异常。
- type:string:与上面一致,不过会先调用 obj 的 toString 方法,再调用 obj 的 valueOf 方法。
什么情况需要做隐式类型转换:
基本类型的隐式转换,主要发生在 + - * / 以及 == > < 这些操作符中,会进行隐式类型转换:
+操作符,左右有一个为 string 类型 两边变量就会被隐式转换为 string 类型;否则转换为 number 类型
'0' + 1 // '01'
1 + false // 1
false + true // 1
- * /操作符:转换为number 类型
2 - '1' // 1
1 * false // 0
2 / 'a' // NaN
- == 操作符:转换为 number
3 == true // false:true 转换为 1 ,再与 3 比较
'0' == false // true:'0' 转换为0,false 转换为 0 ,再进行比较
'0' == 0 // true:'0' 转换为 0,再进行比较
- < > 操作符:转换为 number,如果两边都是 String 类型 则比较字母表顺序
'12' < 13 // true
false > -1 // true
'a' < 'b' // true
引用数据的隐式转换: 通过 toPrimitive 转换为基本类型,再进行转换。
const a = {}
a > 2 // false
通过 toPrimitive 方法,先调用 valueOf() 方法 返回 {};
再调用 toString() 方法 返回 '[object object]' 字符串;
再将其转换为number 为 NaN
NaN > 2 ,返回 false
var b = { name: 'a' };
var c = { age: 20 };
a + b
toPrimitive type 默认 number
先调用 valueOf 方法 返回 {},再调用 toString 方法返回 '[object object]';
两者相加:'[object object][object object]'
11、+ 操作符什么用于字符串拼接?
- 如果某个操作是字符串
- 如果其中一个操作是对象(包括数组),会通过 toPrimitive 进行数据的隐式转换,先转换为字符串类型,如果不行再转换为数字类型的数据。
12、object.assgin、扩展运算符 是深拷贝还是浅拷贝?
- 两者都是浅拷贝,
- object.assgin:接收第一个参数为目标对象,后面的参数都合并到目标对象。它会修改一个对象,所以会触发ES6 setter。
- 扩展运算符:数组 / 对象 的每一个值都被拷贝到一个新的数组 / 对象中。继承的属性或类的属性不会被拷贝,会拷贝 symbol 属性。
- 如果对象的属性值是基本数据,因为针对属性的拷贝,跟新后的对象发生变化,不会影响原来的数据。所以可以通过其进行单层的对象进行深拷贝。
- 如果是多层级对象(属性值是对象),属性值存储在栈中,拷贝了其指针的指向,跟新后的对象发生变化,会影响原来的数据。
13、什么是 深拷贝、浅拷贝,如何进行深拷贝
- 深拷贝:将内存地址完全拷贝出来,放到新的内存区域中,修改不会影响原对象
- 浅拷贝:只是拷贝出栈中的值,还是指向之前的内存地址,修改会影响原对象
深拷贝方法:
JSON.parse(JSON.stringify(obj))
function haveNewObj3(obj) {
return JSON.parse(JSON.stringify(obj))
}
Object.assign({},obj) 实现对象没有嵌套对象时的深拷贝
function haveNewObj1(obj) {
return Object.assign({}, obj)
}
扩展运算符 实现对象没有嵌套对象时的深拷贝
function haveNewObj2(obj) {
return Array.isArray(obj) ? [...obj] : { ...obj }
}
递归的方式实现
function haveNewObj(obj) {
let newObj = Array.isArray(obj) ? [] : {};
for (const key in obj) {
if (typeof (obj[key]) === 'object') {
newObj[key] = haveNewObj(obj[key])
} else {
newObj[key] = obj[key]
}
}
return newObj
}
14、for...of、for...in、 for...each、find、map 区别
for...in 遍历对象
- 不能通过return结束循环,消耗性能,用于不转换数据的全部遍历。
- 不建议遍历数组:输出的顺序不固定,
- 且遍历对象的值为 null、undefined,for...in 不会执行循环
const obj = { 'a': 1, 'b': 2 };
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
const ele = obj[key];
console.log('ele >>>> ', ele)
}
}
for...of 遍历数组返回 value
- 不能通过return结束循环,消耗性能,用于不转换数据的全部遍历。
- 如果需要遍历对象,结合 Object.keys()、Object.values()、Object.entries() 获取数组形式的key、value, 再进行 for...of 遍历
for (const value of [5, 10, 15, 20]) {
console.log(value)
}
for...each:遍历数组的全部数据
- 不能通过return结束循环,消耗性能,用于不转换数据的全部遍历。
[1, 2, 3, 4].forEach(ele => {
console.log(ele)
});
map:遍历数组,会返回一个新的数组,不改变原始数组
const newArr = [1, 2, 3, 4].map(v => v * 1)
find:对数组进行查找,返回符合条件的 value
const endV = [1, 2, 3].find((item, value, arr) => value > 2)
15、Promise
- Promise 是异步编程的一种解决方案
- 解决了多个请求的嵌套情况,导致回调地域的问题
- Promise 实列的三个状态:Pending(进行中)、Resolved(已完成)、Rejected(已拒绝)
- Promsie 实列有两个过程:pending - resolve(已完成)、pending - reject(已拒绝)
(1) 基本用法:创建 Promise 对象
const p1 = new Promise((resolve, reject) => {
console.log('222')
setTimeout(() => {
reject(new Error('fail'))
}, 3000)
})
const p2 = new Promise((resolve, reject) => {
console.log('111')
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => { console.log(result) })
.catch(error => { console.log(error) })
.finally(()=>{
const loading = false
})
- then、catch、finally
- then:返回resolve 的参数;
- catch:返回 reject 的参数;
- finally:执行完成后,不管是 resolve 还是 reject 都会执行一次。一般用于 loading 等公共方法的执行
(2)语法糖
resolve: Promise.resolve('xxx')
相当于 new Promise(resolve=>{resolve('xxx')})
Promise.reject('ERROR')
相当于 new Promise((_,reject)=>{ reject('ERROR') })
Promise.resolve('success').then(res => { console.log(res) })
Promise.reject(new Error('BOOM!')).catch(error => {
console.log(error)
})
(3)常见方法
all、allSettled、race
all:
- 通过数组的形式传递多个异步请求
- 当所有异步请求完毕,且返回 resolve,通过数组的形式返回结果
- 当有一个异步返回 reject,直接返回最先返回的reject
const p1 = Promise.resolve(111)
const p2 = Promise.resolve(222)
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3)
}, 3000)
})
Promise.all([p1,p2,p3]).then(result => {
console.log(result)
}).catch(error => {
console.log(error)
// [111,222,3]
allSettled:
- 通过数组的形式传多个异步请求
- 所有异步都执行完毕,不管是 resolve、还是 reject 都通过 then 以数组的形式返回
Promise.allSettled([
Promise.reject({ code: 500, msg: '服务异常' }),
Promise.resolve({ code: 200, list: [] }),
Promise.resolve({ code: 200, list: [] })
]).then(res => {
console.log( res)
}).catch(error => {
console.log(error) // 不执行
})
race:
- 通过数组的形式传多个异步请求
- 哪个先执行完成,直接在 then 中返回。
Promise.race([
Promise.reject('race - ERROR'),
Promise.resolve('race - 1'),
Promise.resolve('race - 2'),
]).then(res => {
console.log(res)
}).catch(error => {
console.log(error) // 不执行
})
(4)使用场景
1> request 超时机制
- 通过 Promise - race 实现 axios 的超时处理。
思路:
- 通过 Promise - race 方式,输入 req、delayTime 的 promise 方法。
- 在返回中进行判断哪个是先返回的,进行返回操作/超时返回的处理
const req = () => {
return new Promise((resolve, reject) => {
resolve()
})
}
const delayTime = (time) => {
return new Promise(resolve => {
setTimeout(() => {
resolve({ code: 202 })
}, time)
})
}
Promise.race([req, delayTime]).then(res => {
if (res.code === 202) {
// 请求超时
}else{
// req 返回之后的逻辑
}
})
2> 红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯不断交替重复亮灯? 三个亮灯函数已经存在.
思路:
- 通过 传参 让 Promise 执行对应颜色的灯;
- 不断交替重复执行,则通过 递归的方法实现
三个亮灯函数:
function red() {
console.log('red');
}
function green() {
console.log('green');
}
function yellow() {
console.log('yellow');
}
判断执行灯颜色的 Promise:
function light(color, time) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (color === 'red') red();
if (color === 'green') green();
if (color === 'yellow') yellow();
resolve()
}, time)
})
}
进行灯颜色的执行,且通过递归让其一直执行
const runColoc = () => light('red', 3000)
.then(res => light('green', 2000))
.then(res => light('yellow', 1000))
.then(res => runColoc())
上面步骤的语法糖:
const runColoc = async () => {
await light('red', 3000)
await light('green', 2000)
await light('yellow', 1000)
runColoc()
}
16、async/await 理解
- async/await 为了优化 then 链开发出来的语法糖;作用是用同步方式,执行异步操作
比如需求 先请求完接口1, 再拿接口1返回的数据作为接口2 的参数,使用 async await 代码就会简洁很多
function request(num) {
return new Promise(resolve => {
setTimeout(() => {
resolve(num * 2)
}, 1000)
})
}
async function fn() {
const res1 = await request(1);
const res2 = await request(res1)
console.log('async >>> ', res2)
}
fn()
- async 函数返回的是一个Promise对象,有无值看有无return值,没有return 值就是 undefined
async function fn() { }
console.log(fn)
console.log(fn()) // Promise {<fulfilled>: undefined}
/* 先打印 end,再打印 2:因为 async 返回的是 Promise 属于宏任务,将当前执行栈的内容执行完成后,再去执行宏任务队列任务 */
async function fn2(num) {
return num
}
console.log(fn2(2)) // Promise {<fulfilled>: 2}
fn2(2).then(res => console.log(res)) // 通过 .then 获取返回值
console.log('end ...')
- await 只能在 async 函数中使用,不然会报错;其是在等待 async 返回的结果
- async 函数的调用,不会照成阻塞,它内部的阻塞都被封装在 Promise 中异步执行。
17.await 在等待什么
- await 在等待一个 async 函数的返回值。
- 实际上是等待一个返回值:不仅仅是等待 Promise 返回的结果,也可以是任意表达式的结果。
- 如果等到的不是一个Promise对象, await 运算结果就是等到的结果。
- 如果等到的是一个 Promise 对象,await 会阻塞后面的代码,等待 Promise resolve,得到 resolve 的值,作为 await 表达式运算的结果。
/*
await 后面不接 Promise,打印顺序:end、2、4
因为:先执行执行栈的任务,再执行宏任务队列的任务
*/
function request1(num) {
return num * 2
}
async function fn() {
const res1 = await request1(1);
console.log('res1 >>> ', res1)
const res2 = await request1(2);
console.log('res2 >>> ', res2)
}
fn()
console.log("end")
18、async/await 优势是什么,如何捕获异常
优势
- 处理多个 Promise 组成的 then 链,避免链式调用进行的优化写法。
- 可以通过 try/catch 捕获异常
捕获异常
/* Promise 请求:*/
function request(num) {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('error')
}, 1000)
})
}
/* try/catch 捕获异常 */
async function fn() {
try {
const res2 = await request(2)
} catch (error) {
console.log('error >>>', error)
}
}
fn()
/* 捕获异常的优化写法 */
// 捕获异常的公用方法:
async function errorCaptured(asyncFunc) {
try {
let res = await asyncFunc();
return [null, res]
} catch (e) {
return [e, null]
}
}
// 调用
async function func() {
let [err, res] = await errorCaptured(request);
if (err) {
} else {
console.log('ERROR')
}
}
19、详细说说事件循环机制
JavaScript 是单线程,非阻塞的
浏览器的事件循环:
- 执行栈
- 同步代码的执行,按顺序添加到执行栈中,执行完毕后出栈
2. 事件队列
- 异步代码执行,遇到异步事件不会等待它返回结果,将其挂起,继续执行执行栈的任务。
- 当异步返回结果,将其放在事件队列中,放入队列中不会立即执行回调,
- 继续等待当前执行栈的所有任务执行完毕,主线程空闲状态,
- 主线程会查找事件队列中是否有任务,有则取出排在第一位的事件,并把这个事件对应的回调放在执行栈中,同步执行代码
3. 宏任务、微任务
-
不同的异步任务被分为微任务、宏任务。一般异步任务的结果会被放到任务队列中,保持先进先出的原则执行。
-
如果需要有优先级高的任务需要尽快执行,就需要微任务。
-
宏任务:settimeout、setinterval、postMessage
-
微任务:Promise、MutationObserver
[HTML5新增:监听dom结构变化]
4. 运行机制
- 执行栈的任务,遇到微任务,将其回调添加到微任务队列,遇到宏任务,将其添加到宏任务队列。
- 执行栈的任务执行完毕后,检查微任务不为空,执行微任务的回调,微任务队列执行完毕后,
- 执行UI渲染工作,
- 检查宏任务,取出最近一个宏任务事件,将回调添加到执行栈中,进行同步执行。
- 开始下一个宏任务
20、JavaScript 脚本延迟加载的方式
script 会阻碍 HTML 解析,只有下载并执行玩脚本,才会继续解析HTML。
- script async:解析HTML 和 下载脚本 并行执行,脚本下载完成后 先执行脚本,再解析HTML。
- script defer:解析HTML 和 下载脚本 并行执行,脚本下载完成后,先将HTML解析完毕后,再执行脚本。
- 使用setTimeout延迟方法,给网页流出加载时间
- 把js外部引入的文件放到页面底部,来让js最后引入,从而加快页面加载速度
21、 ajax、fetch、axios 区别
Ajax: 一种技术统称,主要利用XHR实现网络请求
Fetch:具体API,基于promise,实现网络请求
Axios:一个封装库,基于XHR封装,较为推荐使用
ajax
- ajax 是一个概念,很重要的特性之一就是 实现页面局部刷新,无需重载整个页面。
- 其中 XMLRequect 就是实现 ajax 的一个很好的方式之一。
- 如果请求内部包含请求,就会照成回调地狱。
function ajax(url) {
// 1、创建 xhr
const xhr = new XMLHttpRequest();
// 2、提供请求路径
xhr.open('get', url, true);
// 3、接收返回值
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) { // 读取服务器响应结束
if (xhr.status === 200) {
console.log('返回==', xhr.response)
}
}
}
xhr.send(null);
}
fetch:
- ES6出现的,基于 promise 的 真实API;
- 语法简洁,更加语意化。
- 使用 promise,采用 .then 链式调用的方式处理结果,利于代码的可读 以及 解决回调地狱问题;支持 async/await.
- 只对网络请求报错,返回 400 / 500 都认为请求成功;
- 不支持超时控制,使用 Promise.reject 并不能阻止请求过程的继续执行,照成了流量的浪费。
axios:
- 基于 promise 封装的 网络请求库,是基于 XHR 进行的二次封装。
- 浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API;
- 可进行请求拦截、响应拦截
- 可以取消请求
- 自动转换 JSON 数据
22、变量提升
什么是变量提升:
- js 在代码执行前,将变量、函数的申明提升到代码开头。变量被提升会设置默认值 undefined。
- js 变量提升 提高了性能、容错性也更好。
- js 的变量提升导致了许多与实际感觉不相同的结果,这是JS 的一个设计缺陷,通过 ES6 块级作用域优化了这个问题。
变量提升本质原因:
- js 的编译过程,需要经过编译、执行阶段。
- 在编译阶段,会收集所有的变量、函数,将其提升到执行上下文的变量环境顶端,这些变量在全局作用域/函数作用域 内任意地方都可被访问。
- 剩下的语句需要等到执行阶段,等到执行具体的某一句才会生效。
变量提升的好处:
- 提高性能:
- 在代码执行前只需操作一次 语法的检查 和 预编译,将变量、函数 进行提前声明避免了每次执行代码前都需要解析一次变量、函数,提高了性能。
- 并对函数代码进行压缩、去除注释、不必要空白等,每次执行函数就可直接为函数分配栈空间(避免了再次解析),代码执行变快。
- 容错性好:
- 对于一些未声明直接使用的变量,不会直接报错。其容错性更好。
变量提升导致的问题:
(1)变量被覆盖:全局声明了变量,函数内部声明变量,再函数作用域中,变量为函数上下文的值。有时会照成歧义
var a = '11';
function fn() {
console.log(a); // undefined
if (false) var a = '22';
}
fn();
console.log(a); // '11'
(2) 遍历时,变量提升为全局变量,在函数结束时并不会被销毁。
for (var a = 0; a < 5; a++) {
// ...
}
console.log(a) // 5
23、尾调用
什么是尾调用
- 在调用的函数中,函数的最后一步是调用另一个函数。最后一步一定要是 return 一个 fn()。
- 尾调用只在严格模式下有效,正常模式无效。
fn(){
...
return fn1()
}
尾调用作用
- 函数在调用时,会在内存形成调用记录,称为调用桢。在函数层层调用时,会对调用桢进行层层叠加,就形成了调用栈。
- 在上一层的函数调用完成后才会返回上一层,继续执行。
- 如果使用尾调用,在最后一步执行函数后,会将当前函数进行消除,将尾调用的函数放入调用栈,对性能做了优化。
例如:函数A对函数B进行调用
- 会在调用函数A时, 将函数A放入调用栈;调用函数B 时,也会将函数B 放入调用栈;当函数B 调用完成后,再从栈中取出 函数A、函数B。
- 如果使用尾调用,调用函数A 将函数A放入调用栈;执行函数 A 时 发现尾调用函数B【B 为 A 的最后一步执行】,就会将函数A 从调用栈中移出,将函数B 放入调用栈;这样就做到了性能优化。
尾递归
- 函数内部调用自身,就是递归;函数内部尾调用自身就是尾递归。
- 因为递归 产生了许多的执行栈,很容易导致栈溢出错误;尾递归 只会有一个执行栈,完美规避了栈溢出的问题。
24、原型、原型链
原型、原型链的理解:
原型:
- 在 js 中,每个对象都有一个 prototype 属性,这个属性指向一个对象,被称为原型对象;
- 原型对象的作用:用来存放实列对象所以公用的属性、方法;
- 原型对象,有个 constructor 属性,指向它的构造函数;
constructor、prototype和__proto__之间的关系
- 显示原型:用 prototype 查找原型,这个是函数类型数据的属性;
- 隐式原型:用
__proto__查找原型,这个属性指向当前构造函数的原型对象;这个是对象类型属性的属性;
所以 per.proto === Person.prototype
三者的关系为:
- 通过 显示原型 获取构造函数的原型对象;
- 通过 隐私原型 获取 实列对象对应的构造函数的原型对象;
- 通过 显示原型的属性 constructor 获取 原型对象指向的构造函数;
原型链
- 每一个对象都有原型对象 prototype,所以原型对象也有 指向自身的 原型对象;
- 查询某个对象的属性,会通过
__proto__指向 原型对象,使用__proto__查找到当前构造函数的原型对象; - 如果查找不到,会在原型对象的原型对象查找;直到查找到属性返回 或者 查找到最终 对象的 原型对象,最终 Object 的原型对象 的原型指向 Object.prototype 其原型对象为 null,所以返回 undefined;
- 这个过程就是原型链,因为是通过
__proto__查找,也叫隐式原型链。
function Person(name, age) {
this.name = name;
this.age = age;
}
/* 创建公共的 prototype 对象的属性、方法 */
Person.prototype.species = '人类';
Person.prototype.say = function () {
console.log("Hello");
}
/* 创建一个实列 */
let per = new Person('xiaoming', 20);
/* 可以通过 constructor 获取实列对应的构造函数 */
per.constructor // Person()
/* 实列的隐私原型 指向 构造函数的原型 */
per.__proto__ === Person.prototype
原型的修改、重写
// 修改原型,在原型上添加一个方法 age
Person.prototype.age = 20;
重写原型后,因为是直接通过对象给 原型对象赋值,所以 xx.constructor 指向的构造函数变成了 Object 根构造函数,需要重新指回当前构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
getName: function () {
console.log(this.name);
}
}
const p = new Person("张三");
// 隐式原型 仍等于 显示原型
p.__proto__ === Person.prototype
// constructor 指向的构造函数变为 Object,需要重新指回来
p.constructor = Person;
原型链的终点是什么,如何打印?
- 原型链的所以原型都是 Object,所以原型链终点是:
Object.prototype.__proto__,其值为 null,所以原型链的终点为 null。 - 打印:
Object.prototype.__proto__
如何获取非原型链的对象属性
通过 hasOwnProperty() 方法可以判断属性在对象本身而不是原型链;
obj.hasOwnProperty(key) 或者 Object.hasOwnProperty.call(obj, key)
function haveOwnFn() {
let res = [];
for (const key in object) {
if (Object.hasOwnProperty.call(object, key)) {
res.push(key + ':' + object[key])
}
}
return res;
}
25、闭包
1、什么是闭包
有权限访问另一个函 数作用域中变量的函数。 常用的方法:在一个 fn 内部调用另一个fn2,则在 fn2 中可以访问 fn 的内部变量
2、闭包的作用
1、在函数执行结束后,其内部执行上下文的变量仍然存储在内存中。 因为内部函数fn2 调了函数 fn1 的变量,所以这个变量性不会被回收
2、可以在函数外部访问内部的变量。可以用来创建私有变量
function a() {
var a = 1;
window.fnb = function () {
console.log(a)
}
}
a()
fnb()
3、经典面试题:
循环中使用闭包解决 var 定义函数的问题 setTimeout 是异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
解决方案,使用闭包的方式:
for (var i = 1; i <= 5; i++) {
(function timer(j) {
setTimeout(() => { console.log(j) }, j * 1000)
})(i)
}
或者使用 let 的块级作用域解决:
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
26、作用域、作用域链
什么是作用域:
- 程序中定义变量的区域,限定这个变量可用性的范围,就是作用域。
- javascript 的作用域是静态作用域,
- 作用域是分层的,内部作用域可以访问外部作用域变量,外部作用域 不可访问内部作用域变量。
全局作用域:
- 最外层定义的 函数、变量 拥有全局作用域;
- 所有未声明直接赋值的变量 会被状态提升为全局作用域
- windows 对象的属性、方法拥有全局作用域
函数作用域:
- 在函数内部声明的变量,拥有函数作用域,一般只能在函数内部访问。
块级作用域:
- ES6 新增 const、let 声明块级作用域,块级作用域在 {} 内。
- const、let 不会变量提升,不可重复声明,存在暂时性死区,
- 循环中适合使用块级作用域,这样就可把声明的变量作用域限制在循环内部。
作用域链:
- 在该层级作用域中查找变量,如果查找不到;就会去上一层级作用域中查找,直到找到变量 或者查寻到 全局作用域,这个查找到过程就是作用域链。
27、执行上下文
什么是执行上下文
在 jsvascript 代码执行前,会先进行代码的解析,即预编译阶段。解析时会先创建一个全局执行上下文, 将代码中 即将执行的变量进行变量、函数声明,变量初始值设置为undefiend; 函数会先创建为函数执行上下文; 再进行程序的执行。
执行上下文分为三类
- 全局执行上下文
- 是默认执行上下文,一个程序只存在一个全局执行上下文。
- 函数执行上下文:
- 当一个函数被调用时,就会为该函数创建一个新的函数执行上下文,一个函数的执行上下文可以有多个。
- Eval 函数执行上下文:
- eval 函数中的代码创建 eval 函数执行上下文,不常用
什么是执行上下文栈
- 当js 执行代码时,会先遇到全局代码,创建一个全局执行上下文并压入
执行栈中; - 遇到函数调用,会为该函数创建一个新的执行上下文,并压入
执行栈栈顶; - 引擎会执行 执行栈栈顶的函数,函数执行完毕后,执行上下文从栈内弹出,继续执行下一个执行上下文。
- 当所有代码执行完毕,会从引擎弹出全局执行上下文。
28、this、call、apply、bind
this:用远指向最后调用它的那个对象。
改变 this 指向:
- 使用 ES6 箭头函数 - 箭头函数本书没有 this,指向最近一层的 this
- 函数内部使用 _this = this - 将 this 保存在变量 _this 中,然后在需要的地方进行使用
- 使用 call、apply、bind
fn.call(thisFn,data1,data2...)
fn.apply(thisFn,[data]) 将 fn 的 this 指向 thisFn,数组的形式传参;
fn.bind(thisFn,data1,data2...)
- 第一个参数都是 需要指向 的 Fn,如果不传 或者 参数为 null、undefined 则指向 window
- apply 以数组的形式传参数,其他两个以参数列表的形式传参;
- call、apply 改变 this 后,函数会立马执行
- bind,改变 this 后,需要手动调用 函数的执行。
29、数组去重
1、新键一个数组,使用 includes 或者 indexOf 查询新的数组没有数组遍历内部对应的值,就将其 push 到新的数组中。
function flterArrFn(arr) {
let arrFilter = [];
arr.forEach(ele => {
console.log('arr.includes(ele)', arr.includes(ele))
/* includex:某个字段是否存在 数组中,返回 Boolean */
// if (!arrFilter.includes(ele)) arrFilter.push(ele)
/* indexOf:某个字段在数组中的下标,返回具体下标 / -1 */
if (arrFilter.indexOf(ele) === -1) arrFilter.push(ele)
});
return arrFilter
}
const arr = [1, 2, 3, 3, 4, 5, 6, 4, 3, 0]
console.log(flterArrFn(arr))
2、使用 filter 返回符合条件的数组:即新键的数组没有 当前数组内的item
arr.filter((item, index, arr) => {
return arr.indexOf(item, 0) === index
})
30、数组扁平化
1、ES6 的 flat - Infinity
const arr1 = [1, 2, 3, [4, 5, [6, 7, [8]]], 9]
let flatArr = arr1.flat(Infinity)
2、利用递归方法
forEach 数组,item 为基本数据类型,这直接 push 到 newArr;为数组重复这个方法,将返回的值 与 newArr 的值进行合并;最后返回 newArr.
合并的方法,可以使用 concat 或者 扩展运算符 实现。
function flatFn(arr) {
let flatArr = [];
arr.forEach(ele => {
if (Array.isArray(ele)) {
// flatArr = flatArr.concat(flatFn(ele))
flatArr = [...flatArr, ...flatFn(ele)]
} else {
flatArr.push(ele)
}
});
return flatArr
}
20、从浏览器地址栏输入 url 到请求翻译发生了什么
- 输入URL,通过DNS解析成IP地址;
- 与服务端 TCP 三次握手,建立连接
- 发送 HTTP 请求,服务器处理请求,并返回 HTTP 报文;
- 对报文进行解析,并在浏览器进行展示
- 与服务端四次握手,断开连接
什么是URL
- url 俗称网址:是统一资源定位符,用于定位互联网的资源。
- 由 协议、域名、端口号、服务器上的路径、资源名称、参数 等组成
scheme: // host.domain:port / path / filename ? abc = 123 # 456789
scheme - 定义因特网服务的类型。常见的协议有 http、https、ftp、file,
其中最常见的类型是 http,而 https 则是进行加密的网络传输。
host - 定义域主机(http 的默认主机是 www)
domain - 定义因特网域名,比如 baidu.com
port - 定义主机上的端口号(http 的默认端口号是 80)
path - 定义服务器上的路径(如果省略,则文档必须位于网站的根目录中)。
filename - 定义文档/资源的名称
query - 即查询参数
fragment - 即 # 后的hash值,一般用来定位到某个位置
DNS如何解析 参考文献
DNS介绍:
- 什么是DNS:用户输入一个 url 时,浏览器并不能直接通过 url 找到对应的服务器,需要转换主机对应的IP地址。
- DNS是分布式的:每台DNS服务器只负责部分映射,所以是分布式存储在不同的服务器上。
- DNS服务器的层次:分为 根DNS服务器、顶级域DNS服务器、权威DNS服务器。
- 本地DNS服务器:当主机发送 DNS 请求时,该请求被发往本地DNS服务器,本地 DNS 服务器起着代理的作用 将请求 转发 到DNS服务器层级中,获取最终IP
如何解析成IP:
- 主机向 本地DNS服务器 发送DNS查询报文;
- 本地 DNS服务器 将报文转发到 根DNS服务器;
- 根DNS服务器 注意到 com 前缀,便向本地 DNS服务器 返回 com 对应的 DNS顶级域 对应IP列表;本地DNS服务器便向 顶级域NDS服务器 发送查询报文;
- 顶级域 DNS 注意到 baidu.com 前缀,便向本地DNS服务器返回 baidu.com 对应的 DNS权威域 对应的 IP 列表;本地 DNS服务器向 权威DNS服务器 发送查询报文;
- 最终返回了 www.baidu.com 的 IP
- 本地 DNS服务器将 IP 返回给 主机,就可以通过 IP 发送请求,获取对应的内容。
- 主机向本地 DNS服务器 查询就是 递归查询 ,后面的三个查询就是 迭代查询
DNS缓存:
- 为了更快速的拿到IP,在进行DNS查询时,最终获取到了IP后,会将对应的映射缓存在本地;
- 下次查询就用缓存的内容,每一个缓存映射都有过期时间的。
TCP三次握手
- 第一次握手,浏览器发送数据包到服务器,告诉服务器,浏览器要发起请求了;
- 第二次握手,服务器发起响应包以表示信息的传达,告诉浏览器,接收到通知,可以发送请求;
- 第三次握手,浏览器再次发起一个数据包表示握手结束,告诉服务器,收到消息,马上发送请求。
三次握手目的:
客户端和服务端要进行可靠传输,避免丢包现象。需要确定双发的接收能力和发送能力.
第一次握手确定客户端的发送能力;第二次握手确定服务端的接收能力和发送能力;第三次握手确定客户端的接收能力。
浏览器解析渲染页面
- 1、解析HTML构建dom树;
- 2、解析css 生成 css 规则树;
- 3、合并 dom 树 和 css规则树,生成 render 树;
- 4、布局 render 树,进行各元素的 尺寸、位置计算;
- 5、绘制 render 树,绘制页面像素信息。
TCP关闭连接,四次握手
- 浏览器发送报文,表示没有数据传输了。并进入 fin_wait_1 状态;
- 服务器发送报文,表示同意关闭请求,发起方进入 fin_wait_2 状态;
- 服务器发送报文,请求关闭连接,并进入 lose_wait 状态;
- 浏览器发送报文,进入等待 time_wait 状态,服务器接收报文关闭连接,浏览器等待一段时间后未收到回复,正常关闭连接。
21、垃圾回收机制
什么是GC
- 程序运行过程中会产生很多垃圾,这些垃圾是程序不用的内存或者是之前使用过的之后不再会使用的内存,GC 用了回收这些垃圾。
- 通过垃圾回收机制将这些内存进行释放,防止内存占用过高 影响系统性能 甚至导致 进程崩溃的情况。
垃圾回收策略
- 定期找出 不可达性的对象(无用的内存)并进行释放,有两种策略进行回收:
标记清除法和引用计数法。 - 在 JavaScript 中 标记清除法 是最常用的,各浏览器会对其进行优化加工,达到更好的清除效果。
标记清除法:
- 垃圾收集器在运行时,给每个内存都添加上一个标记,会假设全部为垃圾,全部标记为0,
- 然后对 各个根对象进行遍历,将不需要回收的内存 标记改为 1,
- 清除所有 标记为0 的内存,对其内存进行销毁 并 回收,
- 最后,把所有内存标记改为 0,等待下一轮回收。
优点:其做法简单,只需将其标记为0 / 1 ,再进行回收即可。
缺点:
- 内存碎片话:垃圾回收后,剩余的内存位置是不变的,其生余内存位置不是一整块,由不同大小的内存块组成了一个列表,就形成了
内存碎片; - 再新建对象的时候,需要对
内存碎片进行遍历,找到符合当前大小的内存块,也就是需要对内存进行分配; - 分配速度慢:对于大内存的对象,其最坏的打算就是会遍历到最后,分配效率会比较慢。
可以通过 标记整理算法 对标记清除法进行优化:
在将标记完成后,将不需要清除的对象(标记为 1),移至内存的一端,清理掉边界内存,就可以有效解决标记清除法的缺点。
引用计数法
- 申明一个变量,并且赋值的时候,这个值的引用次数就是 1,
- 如果一个值被赋予给另外一个变量,引用次数就 + 1,
- 如果值被其他值覆盖,引用次数 - 1,
- 引用次数为 0 时,说明变量没有在使用,这个值不能再被访问,垃圾回收就会清除 引用次数为 0 的变量的内存。
优点:当引用计数为0 时,可以立即被回收垃圾
缺点:
- 需要一个计数器,需要占很大内存,因为不知道引用数据的数量上限;
- 对于循环引用,无法解决回收问题,达不到回收效果。
V8引擎 对 GC优化
分代式垃圾回收
对于 大、老、存活时间长的对象 与 新、小、存活时间短的 对象,进行区分开,进行不同的 GC 处理;
- 将整个内存分为:
新生代、老生代;新生代 又分为:使用区、空闲区。- 新对象都存储在使用区,当使用区的内存快占满时,会对使用区进行标记。再将使用区的对象复制到空闲区,将标记为 0 的对象进行垃圾回收。将空闲区与使用区角色替换。
- 当一个对象 多次替换仍然存在 或 复制到空闲区占用空闲区内存超出 25% 就放在老生代中;老生代中采用 原始的 标记清除法进行 GC。
并行回收
- JavaScript 是单线程语言,运行在主线程上;
- 所以进行 GC 时就 阻塞 脚本的执行,等待 GC 完成后再恢复脚本的执行。如果 GC 时间过长,就会造成卡顿。
- 所以在 GC 时,开启多个辅助线程,同时进行 GC 操作,就会缩短 GC 时间。
增量标记回收
- 将一次 GC 分为很多小步,每执行一小步就让应用逻辑执行一小会,这样交替完成 GC 操作。
22、内存泄露
1、什么是内存泄露
- 由于疏忽或者错误,照成程序没有释放已经不需要使用的内存,照成内存的浪费。
- 会减少可用内存的大小,从而降低性能,甚至严重会照成卡顿、程序崩溃的现象。
2、哪些情况可能会导致内存泄漏
- 意外的全局变量,由于使用未声明的变量,而意外状态提升导致创建了一个全局变量,这个变量一直留在内存中无法回收;
- 计时器或回调函数:设置了定时忘记取消,或者循环函数调用了外部变量的引用,这个变量就会一直留在内存中,无法被回收;
- 闭包:不合理使用闭包,从而导致某些变量一直留在内存中
什么是跨域,如何解决?
什么是跨域?
浏览器向服务器进行请求时,需要满足同源策略,如果不满足就形成了跨域问题。
同源策略指:协议、域名、端口号 需要保持一致。
同源策略的限制:
- cookie、localstorage、indexDB 都无法获取
- DOM 和 js 无法获取
- Ajax 请求具有同源策略限制
如何解决 跨域问题
1、使用 JSONP 解决
利用 <script> 没有跨域的限制,在 src 属性中发送带有 callback 的 get 请求,并且接收服务端返回请求返回到 callback 中。JSONP 只支持 GET 请求。
this.$http = axios;
this.$http.jsonp('http://www.domain2.com:8080/login', {
params: {},
jsonp: 'handleCallback'
}).then((res) => {
console.log(res);
})
2、使用 CORS(跨域资源共享) 解决跨域
- 使浏览器向服务器发送 XMLHttpRequest 请求,从而避免了 ajax 需满足同源策略的局限性。
- CORS 需要服务器、浏览器的同时支持
- 请求分为简单请求、非简单请求(对服务器有特殊要求的请求,比如 DELETE、PUT 请求等)
3、使用 node 代理
同源策略是针对浏览器对服务器的请求,服务器之间的请求不受同源策略限制。 通过 node 建立代理服务器 的方式进行解决。
代理服务器的工作:
1、接收浏览器发送的请求
2、向服务器发送请求
3、接收服务器返回的数据
4、向浏览器返回数据
4、nginx 代理
与node 中间代理原理相同,通过搭建中间件 nginx 服务器,用于转发请求。 只需要进行修改 nginx 的配置即可解决跨域问题,
proxy: {
'/api': {
'target': 'http://jsonplaceholder.typicode.com/', // 需要代理的服务器地址
'changeOrigin': true, // 确定代理
'pathRewrite': { '^/api': '' }, // 路径重写:使用代理后的路径为 http://jsonplaceholder.typicode.com/
},
},
ES6
1、const、var、let 区别
- 变量申明:var 可以不进行变量声明,直接使用,const、let 必须声明了之后才可以使用
- 初始化:var、let 可以不进行初始化赋值,会进行变量提升到全局;const 必须进行初始化赋值否则报错
- 作用域:var 作用域为 全局 或者 函数作用域;const、let 新增了 {} 的块级作用域
- 指针指向:const 声明的变量不允许改变指针指向,let 的变量允许改变指针指向
2、const 对象的属性可以修改吗:
- const 是指针指向的内存地址不可改动。
- 如果 const 一个 引用类型的数据,数据的改变,不会导致报错。因为引用类型的数据存储在栈中,其指针的指向未发生改变。
3、new 操作符的实现原理?如果 new 一个箭头函数,会怎么样?
实现原理
- 创建一个空对象
- 将构造函数的原型赋给新对象
- 构造函数的 this 指向新对象
- 返回新对象
实现 new
function newFn() {
// 创建一个空对象
let obj = new Object();
// 将构造函数的原型 赋给 空对象的原型链
Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
// 绑定this,让this指向新的对象
// var ret = Constructor.apply(obj,arguments)
// var ret = Constructor.call(obj,arguments[0],arguments[1],arguments[2])
var ret = Constructor.call(obj, ...arguments)
// 确保 使用new返回的是一个 object 对像
return typeof ret === "object" ? ret : obj;
}
new 一个箭头函数
会报错,因为箭头函数本身没有 this 指向 和 prototype 原型
4、箭头函数和普通函数区别:
- 写法更加简单
- 没有自己的 this,在自己的作用域上一层继承 this,所以其 this 在它定义时就已经固定不会再改变
- call、apply、bind 方法不能改变箭头函数的 this 指向
5、扩展运算符的理解,及使用场景
- 对象、 数组的拷贝。
多个参数的传入,可通过扩展运算符的方法,其中对象属性重复的话,后面的属性会覆盖前面的属性。 - 多个对象、数组的合并,
- 非嵌套数组、对象 可通过 扩展运算符实现深拷贝
- 字符串使用扩展运算符,转换为数组
const strArr = [...'123']
- rest 用于函数传参,不确定参数个数的情况下。打印会以数组的形式返回
function restFn(...args) {
console.log(args) // 以数组的形式返回
}
6、解构赋值理解
通过解构赋值,可以将属性/值从对象/数组中取出,赋值给其他变量。简化代码。
7、ES6 常见语法
1、string:
- 模板字符串,通过
${}的形式 - includes 判断字符串的是否包含另外一个字符串
- startWidth、endWidth 判断字符串是否以某个字符开头 / 结尾
- padStart(2,'0')、padEnd(2,'0') 字符串长度不够,在开头 / 结尾通过某个字符补齐
- trimStart()、trimEnd 删除字符串开头 / 结尾 空格
'abc'.includes('a')
'abc'.startsWith('a')
'abc'.endsWith('c')
const str1 = '1'.padStart(2, '0')
const str2 = ' aa '.trimStart().trimEnd()
2、Number
- Number.isNaN() 判断是否是 NaN
- Number.parseInt()、Number.parseFloat:目的减少全局方法,使语言模块化
- Math.trunc 转换为整数
- Math.sigin() 判断数据是 正数(1)、负数(-1)、零(0)、负零(-0)、NaN
- 进制转换 Number('0b11') Number('0o11') Number('0x11') 为分别将二进制11、八进制11、十六进制11 转换为 十进制
Number.isNaN('') // false
Number.parseInt('12.1xx') // 12
Number.parseFloat('12.1xx') // 12.1
Math.sign(12) // 1
Math.sign(-11) // -1
Math.sign(0) // 0
Math.sign(-0) // -0
Math.trunc(11.11) // 11
3、数组
- find、findIndex
- entries(),keys() 和 values():通过 forOf 实现对 键值、键、值 的遍历
- 扩展运算符、解构赋值
[1, 5, 10, 15].find((item, value, arr) => value > 9) // 返回符合标准的值 10,没有则返回 undefined
[1, 5, 10, 15].findIndex((value, index, arr) => value > 9) // 返回符合标准的下标,没有则返回 -1
for (const index of [1, 5, 10, 15].keys()) {
console.log('item >>>', index) // 返回下标
}
for (const ele of [1, 5, 10, 15].values()) {
console.log('value >>>>', ele) // 返回值
}
for (const [index, ele] of [1, 5, 10, 15].entries()) {
console.log('inde、ele >>>', index, ele) // 返回下标、值
}
4、对象
- 属性的简洁写法,扩展运算符、解构赋值
- Object.is() 比较两个值是否想等,相当于 === 判断,优化了两项:Object.is(NaN,NaN) 为 true,Object.is(-0,+0) 为 false
- Object.assign() 对象的合并
- Object.keys(),Object.values(),Object.entries():通过数组的形式返回对象的 键、值、键值
for (const index of Object.keys({ a: 1, b: 2, c: 3 })) {
console.log('index >>> ', index) // 返回 index
}
for (const value of Object.values({ a: 1, b: 2, c: 3 })) {
console.log('value >>> ', value) // 返回 value
}
for (const [index, values] of Object.entries({ a: 1, b: 2, c: 3 })) {
console.log('index、value >>> ', [index, values]) // 以数组的形式返回 index、value
}