前言
笔者在学习前端时遇到了许许多多问题,也解决了不少。22年快要到了,也得开始准备实习,春招秋招。
但是又不知道面试会考哪些知识点,不知道如何准备面试,非常烦恼。
所以我打算将一些常见的问题抛出,重要的部分会给出详细的解析。
问题解析
一、const、let 和 var 的区别
ES6 附带的功能之一是添加了let和const,可用于变量声明。 问题是,它们与var有何不同?
var
在 ES6 出现之前,必须使用 var 声明。 但是,前端开发出现的一些问题与使用 var 声明的变量有关。 这就是为什么必须要有新的方法来声明变量。 首先,让我们在讨论这些问题之前更多地了解 var。
var 的作用域:
作用域本质上是指变量/函数可供访问的范围。 var可以在全局范围声明或函数/局部范围内声明。
当在最外层函数的外部声明var变量时,作用域是全局的。 这意味着在最外层函数的外部用var声明的任何变量都可以在windows中使用。
当在函数中声明var时,作用域是局部的。 这意味着它只能在函数内访问
var greeter = 'hey hi';
function newFunction() {
var hello = 'hello';
}
这里,greeter是全局范围的,因为它存在于函数外部,而hello是函数范围的。 因此,我们无法在函数外部访问变量hello。 因此,如果我们这样做:
console.log(hello); // error: hello is not defined
var 变量可以重新声明和修改
这意味着我们可以在相同的作用域内执行下面的操作,并且不会出错
var greeter = 'hey hi';
var greeter = 'say Hello instead';
console.log(greeter); // say Hello instead
greeter = 'say ayo instead';
console.log(greeter); // say ayo instead
var 的变量提升:
变量提升是 JavaScript 的一种机制:在执行代码之前,变量和函数声明会移至其作用域的顶部。 这意味着如果我们这样做:
console.log(greeter);
var greeter = 'say hello';
上面的代码会被解释为:
var greeter;
console.log(greeter); // greeter is undefined
greeter = 'say hello';
因此,将var声明的变量会被提升到其作用域的顶部,并使用 undefined 值对其进行初始化.
let
let现在已经成为变量声明的首选。 这并不奇怪,因为它是对var声明的改进。 它也解决了我们刚刚介绍的var问题。 让我们考虑一下为什么会这样。
let 是块级作用域
块是由 {} 界定的代码块,大括号中有一个块.大括号内的任何内容都包含在一个块级作用域中.
因此,在带有let的块中声明的变量仅可在该块中使用。
let greeting = 'say Hi';
let times = 4;
if (times > 3) {
let hello = 'say Hello instead';
console.log(hello); // "say Hello instead"
}
console.log(hello); // hello is not defined
我们看到在其代码块(定义它的花括号)之外使用hello会返回错误。 这是因为let变量是块范围的.
let 可以被修改但是不能被重新声明
就像var一样,用let声明的变量可以在其范围内被修改。 但与var不同的是,let变量无法在其作用域内被重新声明。 来看下面的例子:
let greeting = 'say Hi';
let greeting = 'say Hello instead'; // error: Identifier 'greeting' has already been declared
但是,如果在不同的作用域中定义了相同的变量,则不会有错误:
let greeting = 'say Hi';
if (true) {
let greeting = 'say Hello instead';
console.log(greeting); // "say Hello instead"
}
console.log(greeting); // "say Hi"
为什么没有错误? 这是因为两个实例的作用域不同,因此它们会被视为不同的变量。
这个事实说明:使用let,是比var更好的选择。 当使用let时,你不必费心思考 🤔 变量的名称,因为变量仅在其块级作用域内存在。
let 的变量提升
就像var一样,let声明也被提升到作用域顶部。
但不同的是:
- 用
var声明的变量会被提升到其作用域的顶部,并使用 undefined 值对其进行初始化。 - 用
let声明的变量会被提升到其作用域的顶部,不会对值进行初始化。
因此,如果你尝试在声明前使用let变量,则会收到Reference Error。
const
用const声明的变量保持常量值。 const声明与let声明有一些相似之处
const 声明的变量在块级作用域内
像let声明一样,const声明只能在声明它们的块级作用域中访问
const 不能被修改并且不能被重新声明
这意味着用const声明的变量的值保持不变。 不能修改或重新声明。 因此,如果我们使用const声明变量,那么我们将无法做到这一点:
const greeting = 'say Hi';
greeting = 'say Hello instead'; // error: Assignment to constant variable.
const greeting = 'say Hello instead'; // error: Identifier 'greeting' has already been declared
因此,每个const声明都必须在声明时进行初始化。
当用const声明对象时,这种行为却有所不同。 虽然不能更新const对象,但是可以更新该对象的属性。 因此,如果我们声明一个const对象为
const greeting = {
message: 'say Hi',
times: 4,
};
不能像下面这样做:
const greeting = {
words: 'Hello',
number: 'five',
}; // error: Assignment to constant variable.
但我们可以这样做:
greeting.message = 'say Hello instead';
这将更新greeting.message的值,而不会返回错误。
const 的变量提升
就像let一样,const声明也被提升到顶部,但是没有初始化。
总结
var声明是全局作用域或函数作用域,而let和const是块作用域。var变量可以在其范围内更新和重新声明;let变量可以被更新但不能重新声明;const变量既不能更新也不能重新声明。- 它们都被提升到其作用域的顶端。 但是,虽然使用变量
undefined初始化了var变量,但未初始化let和const变量。 - 尽管可以在不初始化的情况下声明
var和let,但是在声明期间必须初始化const。
二、谈谈基本数据类型
基本数据类型有哪几种
七大基本数据类型:undefined、 null、 boolean、 Number、 String、 BigInt、 Symbol
引用数据类型:Object
基本数据类型判断
typeofinstanceofObject.prototype.toString()
typeof null 返回的是什么,为什么?
typeof null 返回的是 Object,这是因为 Object 在底层存储的低三位机器码为 000,而 null 的存储机器码为全 0,所以用 typeof 判断时会直接判断为 Object
总结一下在基本数据类型判断中
简单来说,我们使用 typeof 来判断基本数据类型是 ok 的,不过需要注意当用 typeof 来判断 null 类型时的问题,如果想要判断一个对象的具体类型可以考虑用 instanceof,但是 instanceof 也可能判断不准确,比如一个数组,他可以被 instanceof 判断为 Object。所以我们要想比较准确的判断对象实例的类型时,可以采取 Object.prototype.toString.call 方法。
三、闭包以及闭包的用法
闭包是啥
量的函数(自由变量既不是函数参数,也不在函数内部的变量,其实就是另外一个函数作用域中的变量。)
由此,我们可以看出闭包共有两部分组成:
闭包 = 函数 + 函数能够访问的自由变量
举个例子:
var a = 1;
function foo() {
console.log(a);
}
foo();
foo 函数可以访问变量 a,但是a不是foo函数的局部变量,也不是foo函数的参数,所以 a 就是自由变量。
那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛……
闭包的用法
我们来看看最常遇到的一个闭包题:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
答案是都是 3,让我们分析一下原因:
当执行到 data[0] 函数之前,此时全局上下文的 VO 为:
globalContext = {
VO: {
data: [...],
i: 3
}
}
简单来讲
当执行到data[0]函数的时候,for循环已经执行完了,i是全局变量,此时的值为3,举个例子:
for (var i = 0; i < 3; i++) {}
console.log(i) // 3
当执行 data[0] 函数的时候,data[0] 函数的作用域链为:
data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。
data[1] 和 data[2] 是一样的道理。
所以让我们改成闭包看看:
var data = [];
// 这里使用IIFE(立即执行函数表达式)当每次for循环时,把此时的i变量传递到func中
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0]();
data[1]();
data[2]();
data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是0。
data[1] 和 data[2] 是一样的道理。
更多详情请见闭包
四、遍历的方法有哪些
常用的遍历方法
for、map、forEach、filter、for...in 、for...of
for
使用临时变量,将长度缓存起来,避免重复获取数组长度,当数组较大时优化效果才会比较明显。
for(var j = 0,j < arr.length;j++) {
//执行代码
}
forEach
遍历数组中的每一项,没有返回值,对原数组没有影响,不支持IE
arr.forEach((value,index,array)=>{
//执行代码
})
//参数:value数组中的当前项, index当前项的索引, array原始数组;
//数组中有几项,那么传递进去的匿名回调函数就需要执行几次;
map
有返回值,可以return出来map的回调函数中支持return返回值;return的是啥,相当于把数组中的这一项变为啥(并不影响原来的数组,只是相当于把原数组克隆一份,把克隆的这一份的数组中的对应项改变了)
arr.map(function(value,index,array){
//do something
return XXX
})
var ary = [12,23,24,42,1];
var res = ary.map(function (item,index,ary ) {
return item*10;
})
console.log(res);//-->[120,230,240,420,10]; 原数组拷贝了一份,并进行了修改
console.log(ary);//-->[12,23,24,42,1]; 原数组并未发生变化
for...of
可以正确响应break、continue和return语句
for (var value of myArray) {
console.log(value);
}
for...in
访问数组的下标,而不是实际的数组元素值:
for (let i in arr) {
console.log(arr[i]);
}
filter
不会改变原始数组,返回新数组
var arr = [73,84,56, 22,100]
var newArr = arr.filter(item => item>80) //得到新数组 [84, 100]
console.log(newArr,arr)
for...in 和 for...of 的区别
for...in 用来遍历对象的 key 值和原型链上的值
for...of 用来遍历对象的 value 值
五、ES6 有哪些新特性
let、const- 箭头函数
promiseasync和awaitset、mapclassSymbol- 解构
箭头函数和普通函数的区别
主要区别在于箭头函数没有 this 和 argument
没有 this 的话
- 不能作为构造函数
- 没有原型
- 无法通过
call、bind、apply改变this指向 - 它的
this永远指向 它定义时所处的全局执行环境 - 箭头函数不绑定
this,会捕获其所在的上下文的this值,作为自己的this值。 - 不可以使用
arguments对象,该对象在函数体内不存在。
但我们可以用展开运算符来接收参数,比如:
const testFunc = (...args)=>{
console.log(args) //数组形式输出参数
}
Promise
new promise时, 需要传递一个executor()执行器,执行器立即执行;
executor接受两个参数,分别是resolve和reject;
务必掌握 Promise 常用方法,如 then ,all,race,resolve,reject
Promise 的关键点在于
- 三个状态
- 链式调用
我们从一个场景开始考虑下面一种获取用户 id 的请求处理:
//不使用Promise
http.get('some_url', function (result) {
//do something
console.log(result.id);
});
//使用Promise
new Promise(function (resolve) {
//异步请求
http.get('some_url', function (result) {
resolve(result.id)
})
}).then(function (id) {
//do something
console.log(id);
})
乍一看,好像不使用 Promise 更简洁一些。其实不然,设想一下,如果有好几个依赖的前置请求都是异步的,此时如果没有 Promise ,那回调函数要一层一层嵌套,看起来就很不舒服了。如下:
//不使用Promise
http.get('some_url', function (id) {
//do something
http.get('getNameById', id, function (name) {
//do something
http.get('getCourseByName', name, function (course) {
//dong something
http.get('getCourseDetailByCourse', function (courseDetail) {
//do something
})
})
})
});
//使用Promise
function getUserId(url) {
return new Promise(function (resolve) {
//异步请求
http.get(url, function (id) {
resolve(id)
})
})
}
getUserId('some_url').then(function (id) {
//do something
return getNameById(id); // getNameById 是和 getUserId 一样的Promise封装。下同
}).then(function (name) {
//do something
return getCourseByName(name);
}).then(function (course) {
//do something
return getCourseDetailByCourse(course);
}).then(function (courseDetail) {
//do something
});
实现原理
说到底,Promise 也还是使用回调函数,只不过是把回调封装在了内部,使用上一直通过 then 方法的链式调用,使得多层的回调嵌套看起来变成了同一层的,书写上以及理解上会更直观和简洁一些。
这时候面试官可能会来一句灵魂之问,你能手写Promise吗👀。。。。?
还在完善中。。。 还不能链式调用的手写Promise
async 和 await
async-await和Promise的关系
经常会看到有了 async-await、promise 还有必要学习吗、async await优于promise的几个特点
我觉得async-await无非就是promise和generator的语法糖。
语法糖这种东西只是为了让我们书写代码时更加流畅,当然也增强了代码的可读性。
简单来说:async-await 是建立在 promise机制之上的,并不能取代其地位。
async函数
async函数是使用async关键字声明的函数。 async函数是AsyncFunction构造函数的实例, 并且其中允许使用await关键字。async和await关键字让我们可以用一种更简洁的方式写出基于Promise的异步行为,而无需刻意地链式调用promise。
async function basicDemo() {
let result = await Math.random();
console.log(result);
}
basicDemo();
// 0.6484863241051226
//Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: undefined}
上述代码就是async-await的基本使用形式。有两个陌生的关键字async、await,同时函数执行结果似乎返回了一个promise对象。
一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,然后返回一个Promise😎,仅此而已。
async function demo01() {
return 123;
}
demo01().then(val => {
console.log(val);// 123
});
若 async 定义的函数有返回值,return 123;相当于Promise.resolve(123),没有声明式的 return则相当于执行了Promise.resolve();
await
await 可以理解为是 async wait 的简写。await 必须出现在 async 函数内部,不能单独使用。
// 正常 for 循环
async function forDemo() {
let arr = [1, 2, 3, 4, 5];
for (let i = 0; i < arr.length; i ++) {
await arr[i];
}
}
forDemo();//正常输出
// 因为想要炫技把 for循环写成下面这样
async function forBugDemo() {
let arr = [1, 2, 3, 4, 5];
arr.forEach(item => {
await item;
});
}
forBugDemo();// Uncaught SyntaxError: Unexpected identifier
await 后面可以跟任何的JS 表达式。虽然说 await 可以等很多类型的东西,但是它最主要的意图是用来等待 Promise 对象的状态被 resolved。如果await的是 promise对象会造成异步函数停止执行并且等待 promise 的解决,如果等的是正常的表达式则立即执行。
function sleep(second) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(' enough sleep~');
}, second);
})
}
function normalFunc() {
console.log('normalFunc');
}
async function awaitDemo() {
await normalFunc();
console.log('something, ~~');
let result = await sleep(2000);
console.log(result);// 两秒之后会被打印出来
}
awaitDemo();
// normalFunc
// VM4036:13 something, ~~
// VM4036:15 enough sleep~
六、原型和原型链
原型链就是由原型通过 proto 连接起来组成的链状结构
笔者还没完全学懂,日后加更。。。😢