先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Web前端全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip1024c (备注前端)
正文
25. 对象的 prototype(原型) 是什么?
简单地说,原型就是对象的蓝图。如果它存在当前对象中,则将其用作属性和方法的回退。它是在对象之间共享属性和功能的方法,这也是JavaScript实现继承的核心。
const o = {};
console.log(o.toString()); // logs [object Object]
即使o
对象中不存在o.toString
方法,它也不会引发错误,而是返回字符串[object Object]
。当对象中不存在属性时,它将查看其原型,如果仍然不存在,则将其查找到原型的原型,依此类推,直到在原型链中找到具有相同属性的属性为止。原型链的末尾是Object.prototype
。
console.log(o.toString === Object.prototype.toString); // logs true
// which means we we’re looking up the Prototype Chain and it reached
// the Object.prototype and used the “toString” method.
26. 什么是 IIFE,它的用途是什么?
IIFE或立即调用的函数表达式是在创建或声明后将被调用或执行的函数。创建IIFE的语法是,将function (){}
包裹在在括号()
内,然后再用另一个括号()
调用它,如:(function(){})()
(function(){
…
} ());
(function () {
…
})();
(function named(params) {
…
})();
(() => {
});
(function (global) {
…
})(window);
const utility = (function () {
return {
…
}
})
这些示例都是有效的IIFE。倒数第二个救命表明我们可以将参数传递给IIFE函数。最后一个示例表明,我们可以将IIFE
的结果保存到变量中,以便稍后使用。
IIFE的一个主要作用是避免与全局作用域内的其他变量命名冲突或污染全局命名空间,来个例子。
假设我们引入了一个omelibr.js
的链接,它提供了一些我们在代码中使用的全局函数,但是这个库有两个方法我们没有使用:createGraph
和drawGraph
,因为这些方法都有bug
。我们想实现自己的createGraph
和drawGraph
方法。
解决此问题的一种方法是直接覆盖:
当我们使用这个解决方案时,我们覆盖了库提供给我们的那两个方法。
另一种方式是我们自己改名称:
当我们使用这个解决方案时,我们把那些函数调用更改为新的函数名。
还有一种方法就是使用IIFE:
在此解决方案中,我们要声明了graphUtility
变量,用来保存IIFE执行的结果,该函数返回一个包含两个方法createGraph
和drawGraph
的对象。
IIFE 还可以用来解决一个常见的面试题:
var li = document.querySelectorAll(‘.list-group > li’);
for (var i = 0, len = li.length; i < len; i++) {
li[i].addEventListener(‘click’, function (e) {
console.log(i);
})
假设我们有一个带有list-group
类的ul
元素,它有5
个li
子元素。当我们单击单个li
元素时,打印对应的下标值。但在此外上述代码不起作用,这里每次点击 li
打印 i
的值都是5
,这是由于闭包的原因。
闭包只是函数记住其当前作用域,父函数作用域和全局作用域的变量引用的能力。当我们在全局作用域内使用var
关键字声明变量时,就创建全局变量i
。因此,当我们单击li
元素时,它将打印5
,因为这是稍后在回调函数中引用它时i
的值。
使用 IIFE 可以解决此问题:
var li = document.querySelectorAll(‘.list-group > li’);
for (var i = 0, len = li.length; i < len; i++) {
(function (currentIndex) {
li[currentIndex].addEventListener(‘click’, function (e) {
console.log(currentIndex);
})
})(i);
}
该解决方案之所以行的通,是因为IIFE会为每次迭代创建一个新的作用域,我们捕获i
的值并将其传递给currentIndex
参数,因此调用IIFE时,每次迭代的currentIndex
值都是不同的。
27. Function.prototype.apply 方法的用途是什么?
apply()
方法调用一个具有给定this值的函数,以及作为一个数组(或类似数组对象)提供的参数。
const details = {
message: ‘Hello World!’
};
function getMessage(){
return this.message;
}
getMessage.apply(details); // ‘Hello World!’
call()
方法的作用和apply()
方法类似,区别就是call()
方法接受的是参数列表,而apply()
方法接受的是一个参数数组。
const person = {
name: “Marko Polo”
};
function greeting(greetingMessage) {
return ${greetingMessage} ${this.name}
;
}
greeting.apply(person, [‘Hello’]); // “Hello Marko Polo!”
28. Function.prototype.call
方法的用途是什么?
call()
方法使用一个指定的 this
值和单独给出的一个或多个参数来调用一个函数。
const details = {
message: ‘Hello World!’
};
function getMessage(){
return this.message;
}
getMessage.call(details); // ‘Hello World!’
注意:该方法的语法和作用与 apply()
方法类似,只有一个区别,就是 call()
方法接受的是一个参数列表,而 apply()
方法接受的是一个包含多个参数的数组。
const person = {
name: “Marko Polo”
};
function greeting(greetingMessage) {
return ${greetingMessage} ${this.name}
;
}
greeting.call(person, ‘Hello’); // “Hello Marko Polo!”
29. Function.prototype.apply 和 Function.prototype.call 之间有什么区别?
apply()
方法可以在使用一个指定的 this
值和一个参数数组(或类数组对象)的前提下调用某个函数或方法。call()
方法类似于apply()
,不同之处仅仅是call()
接受的参数是参数列表。
const obj1 = {
result:0
};
const obj2 = {
result:0
};
function reduceAdd(){
let result = 0;
for(let i = 0, len = arguments.length; i < len; i++){
result += arguments[i];
}
this.result = result;
}
reduceAdd.apply(obj1, [1, 2, 3, 4, 5]); // 15
reduceAdd.call(obj2, 1, 2, 3, 4, 5); // 15
30. Function.prototype.bind 的用途是什么?
bind()
方法创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
import React from ‘react’;
class MyComponent extends React.Component {
constructor(props){
super(props);
this.state = {
value : “”
}
this.handleChange = this.handleChange.bind(this);
// 将 “handleChange” 方法绑定到 “MyComponent” 组件
}
handleChange(e){
//do something amazing here
}
render(){
return (
<>
<input type={this.props.type}
value={this.state.value}
onChange={this.handleChange}
/>
</>
)
}
}
31. 什么是函数式编程? JavaScript 的哪些特性使其成为函数式语言的候选语言?
函数式编程(通常缩写为FP)是通过编写纯函数,避免共享状态、可变数据、副作用 来构建软件的过程。数式编程是声明式 的而不是命令式 的,应用程序的状态是通过纯函数流动的。与面向对象编程形成对比,面向对象中应用程序的状态通常与对象中的方法共享和共处。
函数式编程是一种编程范式 ,这意味着它是一种基于一些基本的定义原则(如上所列)思考软件构建的方式。当然,编程范示的其他示例也包括面向对象编程和过程编程。
函数式的代码往往比命令式或面向对象的代码更简洁,更可预测,更容易测试 - 但如果不熟悉它以及与之相关的常见模式,函数式的代码也可能看起来更密集杂乱,并且 相关文献对新人来说是不好理解的。
JavaScript支持闭包和高阶函数是函数式编程语言的特点。
32. 什么是高阶函数?
高阶函数只是将函数作为参数或返回值的函数。
function higherOrderFunction(param,callback){
return callback(param);
}
33. 为什么函数被称为一等公民?
在JavaScript中,函数不仅拥有一切传统函数的使用方式(声明和调用),而且可以做到像简单值一样赋值(var func = function(){})
、传参(function func(x,callback){callback();})
、返回(function(){return function(){}})
,这样的函数也称之为第一级函数(First-class Function)。不仅如此,JavaScript中的函数还充当了类的构造函数的作用,同时又是一个Function
类的实例(instance)。这样的多重身份让JavaScript的函数变得非常重要。
34. 手动实现 Array.prototype.map 方法
map()
方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
function map(arr, mapCallback) {
// 首先,检查传递的参数是否正确。
if (!Array.isArray(arr) || !arr.length || typeof mapCallback !== ‘function’) {
return [];
} else {
let result = [];
// 每次调用此函数时,我们都会创建一个 result 数组
// 因为我们不想改变原始数组。
for (let i = 0, len = arr.length; i < len; i++) {
result.push(mapCallback(arr[i], i, arr));
// 将 mapCallback 返回的结果 push 到 result 数组中
}
return result;
}
}
35. 手动实现Array.prototype.filter
方法
filter()
方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。
function filter(arr, filterCallback) {
// 首先,检查传递的参数是否正确。
if (!Array.isArray(arr) || !arr.length || typeof filterCallback !== ‘function’)
{
return [];
} else {
let result = [];
// 每次调用此函数时,我们都会创建一个 result 数组
// 因为我们不想改变原始数组。
for (let i = 0, len = arr.length; i < len; i++) {
// 检查 filterCallback 的返回值是否是真值
if (filterCallback(arr[i], i, arr)) {
// 如果条件为真,则将数组元素 push 到 result 中
result.push(arr[i]);
}
}
return result; // return the result array
}
}
36. 手动实现Array.prototype.reduce
方法
reduce()
方法对数组中的每个元素执行一个由您提供的reducer
函数(升序执行),将其结果汇总为单个返回值。
function reduce(arr, reduceCallback, initialValue) {
// 首先,检查传递的参数是否正确。
if (!Array.isArray(arr) || !arr.length || typeof reduceCallback !== ‘function’)
{
return [];
} else {
// 如果没有将initialValue传递给该函数,我们将使用第一个数组项作为initialValue
let hasInitialValue = initialValue !== undefined;
let value = hasInitialValue ? initialValue : arr[0];
、
// 如果有传递 initialValue,则索引从 1 开始,否则从 0 开始
for (let i = hasInitialValue ? 0 : 1, len = arr.length; i < len; i++) {
value = reduceCallback(value, arr[i], i, arr);
}
return value;
}
}
37. arguments 的对象是什么?
arguments
对象是函数中传递的参数值的集合。它是一个类似数组的对象,因为它有一个length属性,我们可以使用数组索引表示法arguments[1]
来访问单个值,但它没有数组中的内置方法,如:forEach
、reduce
、filter
和map
。
我们可以使用Array.prototype.slice
将arguments
对象转换成一个数组。
function one() {
return Array.prototype.slice.call(arguments);
}
注意:箭头函数中没有arguments
对象。
function one() {
return arguments;
}
const two = function () {
return arguments;
}
const three = function three() {
return arguments;
}
const four = () => arguments;
four(); // Throws an error - arguments is not defined
当我们调用函数four
时,它会抛出一个ReferenceError: arguments is not defined error
。使用rest
语法,可以解决这个问题。
const four = (…args) => args;
这会自动将所有参数值放入数组中。
38. 如何创建一个没有 prototype(原型)的对象?
我们可以使用Object.create
方法创建没有原型的对象。
const o1 = {};
console.log(o1.toString()); // [object Object]
const o2 = Object.create(null);
console.log(o2.toString());
// throws an error o2.toString is not a function
39. 为什么在调用这个函数时,代码中的b
会变成一个全局变量?
function myFunc() {
let a = b = 0;
}
myFunc();
原因是赋值运算符是从右到左的求值的。这意味着当多个赋值运算符出现在一个表达式中时,它们是从右向左求值的。所以上面代码变成了这样:
function myFunc() {
let a = (b = 0);
}
myFunc();
首先,表达式b = 0
求值,在本例中b
没有声明。因此,JS引擎在这个函数外创建了一个全局变量b
,之后表达式b = 0
的返回值为0
,并赋给新的局部变量a
。
我们可以通过在赋值之前先声明变量来解决这个问题。
function myFunc() {
let a,b;
a = b = 0;
}
myFunc();
40. ECMAScript 是什么?
ECMAScript 是编写脚本语言的标准,这意味着JavaScript遵循ECMAScript标准中的规范变化,因为它是JavaScript的蓝图。
ECMAScript 和 Javascript,本质上都跟一门语言有关,一个是语言本身的名字,一个是语言的约束条件
只不过发明JavaScript的那个人(Netscape公司),把东西交给了ECMA(European Computer Manufacturers Association),这个人规定一下他的标准,因为当时有java语言了,又想强调这个东西是让ECMA这个人定的规则,所以就这样一个神奇的东西诞生了,这个东西的名称就叫做ECMAScript。
javaScript = ECMAScript + DOM + BOM(自认为是一种广义的JavaScript)
ECMAScript说什么JavaScript就得做什么!
JavaScript(狭义的JavaScript)做什么都要问问ECMAScript我能不能这样干!如果不能我就错了!能我就是对的!
——突然感觉JavaScript好没有尊严,为啥要搞个人出来约束自己,
那个人被创造出来也好委屈,自己被创造出来完全是因为要约束JavaScript。
41. ES6或ECMAScript 2015有哪些新特性?
-
箭头函数
-
类
-
模板字符串
-
加强的对象字面量
-
对象解构
-
Promise
-
生成器
-
模块
-
Symbol
-
代理
-
Set
-
函数默认参数
-
rest 和展开
-
块作用域
42. var
,let
和const
的区别是什么?
var
声明的变量会挂载在window
上,而let
和const
声明的变量不会:
var a = 100;
console.log(a,window.a); // 100 100
let b = 10;
console.log(b,window.b); // 10 undefined
const c = 1;
console.log(c,window.c); // 1 undefined
var
声明变量存在变量提升,let
和const
不存在变量提升:
console.log(a); // undefined ===> a已声明还没赋值,默认得到undefined值
var a = 100;
console.log(b); // 报错:b is not defined ===> 找不到b这个变量
let b = 10;
console.log©; // 报错:c is not defined ===> 找不到c这个变量
const c = 10;
let
和const
声明形成块作用域
if(1){
var a = 100;
let b = 10;
}
console.log(a); // 100
console.log(b) // 报错:b is not defined ===> 找不到b这个变量
if(1){
var a = 100;
const c = 1;
}
console.log(a); // 100
console.log© // 报错:c is not defined ===> 找不到c这个变量
同一作用域下let
和const
不能声明同名变量,而var
可以
var a = 100;
console.log(a); // 100
var a = 10;
console.log(a); // 10
let a = 100;
let a = 10;
// 控制台报错:Identifier ‘a’ has already been declared ===> 标识符a已经被声明了。
暂存死区
var a = 100;
if(1){
a = 10;
//在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域找变量a,
// 而这时,还未到声明时候,所以控制台Error:a is not defined
let a = 1;
}
const
/*
-
1、一旦声明必须赋值,不能使用null占位。
-
2、声明后不能再修改
-
3、如果声明的是复合类型数据,可以修改其属性
-
*/
const a = 100;
const list = [];
list[0] = 10;
console.log(list); // [10]
const obj = {a:100};
obj.name = ‘apple’;
obj.a = 10000;
console.log(obj); // {a:10000,name:‘apple’}
43. 什么是箭头函数?
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this
,arguments
,super
或new.target
。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
//ES5 Version
var getCurrentDate = function (){
return new Date();
}
//ES6 Version
const getCurrentDate = () => new Date();
在本例中,ES5 版本中有function(){}
声明和return
关键字,这两个关键字分别是创建函数和返回值所需要的。在箭头函数版本中,我们只需要()
括号,不需要 return
语句,因为如果我们只有一个表达式或值需要返回,箭头函数就会有一个隐式的返回。
//ES5 Version
function greet(name) {
return 'Hello ’ + name + ‘!’;
}
//ES6 Version
const greet = (name) => Hello ${name}
;
const greet2 = name => Hello ${name}
;
我们还可以在箭头函数中使用与函数表达式和函数声明相同的参数。如果我们在一个箭头函数中有一个参数,则可以省略括号。
const getArgs = () => arguments
const getArgs2 = (…rest) => rest
箭头函数不能访问arguments
对象。所以调用第一个getArgs
函数会抛出一个错误。相反,我们可以使用rest参数来获得在箭头函数中传递的所有参数。
const data = {
result: 0,
nums: [1, 2, 3, 4, 5],
computeResult() {
// 这里的“this”指的是“data”对象
const addAll = () => {
return this.nums.reduce((total, cur) => total + cur, 0)
};
this.result = addAll();
}
};
箭头函数没有自己的this
值。它捕获词法作用域函数的this
值,在此示例中,addAll
函数将复制computeResult
方法中的this
值,如果我们在全局作用域声明箭头函数,则this
值为 window
对象。
44. 什么是类?
类(class)
是在 JS 中编写构造函数的新方法。它是使用构造函数的语法糖,在底层中使用仍然是原型和基于原型的继承。
//ES5 Version
function Person(firstName, lastName, age, address){
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.address = address;
}
Person.self = function(){
return this;
}
Person.prototype.toString = function(){
return “[object Person]”;
}
Person.prototype.getFullName = function (){
return this.firstName + " " + this.lastName;
}
//ES6 Version
class Person {
constructor(firstName, lastName, age, address){
this.lastName = lastName;
this.firstName = firstName;
this.age = age;
this.address = address;
}
static self() {
return this;
}
toString(){
return “[object Person]”;
}
getFullName(){
return ${this.firstName} ${this.lastName}
;
}
}
重写方法并从另一个类继承。
//ES5 Version
Employee.prototype = Object.create(Person.prototype);
function Employee(firstName, lastName, age, address, jobTitle, yearStarted) {
Person.call(this, firstName, lastName, age, address);
this.jobTitle = jobTitle;
this.yearStarted = yearStarted;
}
Employee.prototype.describe = function () {
return `I am ${this.getFullName()} and I have a position of ${this.jobTitle}
and I started at ${this.yearStarted}`;
}
Employee.prototype.toString = function () {
return “[object Employee]”;
}
//ES6 Version
class Employee extends Person { //Inherits from “Person” class
constructor(firstName, lastName, age, address, jobTitle, yearStarted) {
super(firstName, lastName, age, address);
this.jobTitle = jobTitle;
this.yearStarted = yearStarted;
}
describe() {
return `I am ${this.getFullName()} and I have a position of ${this.jobTitle}
and I started at ${this.yearStarted}`;
}
toString() { // Overriding the “toString” method of “Person”
return “[object Employee]”;
}
}
所以我们要怎么知道它在内部使用原型?
class Something {
}
function AnotherSomething(){
}
const as = new AnotherSomething();
const s = new Something();
console.log(typeof Something); // “function”
console.log(typeof AnotherSomething); // “function”
console.log(as.toString()); // “[object Object]”
console.log(as.toString()); // “[object Object]”
console.log(as.toString === Object.prototype.toString); // true
console.log(s.toString === Object.prototype.toString); // true
45. 什么是模板字符串?
模板字符串是在 JS 中创建字符串的一种新方法。我们可以通过使用反引号使模板字符串化。
//ES5 Version
var greet = ‘Hi I’m Mark’;
//ES6 Version
let greet = Hi I'm Mark
;
在 ES5 中我们需要使用一些转义字符来达到多行的效果,在模板字符串不需要这么麻烦:
//ES5 Version
var lastWords = ‘\n’
-
’ I \n’
-
’ Am \n’
-
‘Iron Man \n’;
//ES6 Version
let lastWords = `
I
Am
Iron Man
`;
在ES5版本中,我们需要添加\n
以在字符串中添加新行。在模板字符串中,我们不需要这样做。
//ES5 Version
function greet(name) {
return 'Hello ’ + name + ‘!’;
}
//ES6 Version
function greet(name) {
return Hello ${name} !
;
}
在 ES5 版本中,如果需要在字符串中添加表达式或值,则需要使用+
运算符。在模板字符串s中,我们可以使用${expr}
嵌入一个表达式,这使其比 ES5 版本更整洁。
46. 什么是对象解构?
对象析构是从对象或数组中获取或提取值的一种新的、更简洁的方法。假设有如下的对象:
const employee = {
firstName: “Marko”,
lastName: “Polo”,
position: “Software Developer”,
yearHired: 2017
};
从对象获取属性,早期方法是创建一个与对象属性同名的变量。这种方法很麻烦,因为我们要为每个属性创建一个新变量。假设我们有一个大对象,它有很多属性和方法,用这种方法提取属性会很麻烦。
var firstName = employee.firstName;
var lastName = employee.lastName;
var position = employee.position;
var yearHired = employee.yearHired;
使用解构方式语法就变得简洁多了:
{ firstName, lastName, position, yearHired } = employee;
我们还可以为属性取别名:
let { firstName: fName, lastName: lName, position, yearHired } = employee;
当然如果属性值为 undefined
时,我们还可以指定默认值:
let { firstName = “Mark”, lastName: lName, position, yearHired } = employee;
47. 什么是 ES6 模块?
模块使我们能够将代码基础分割成多个文件,以获得更高的可维护性,并且避免将所有代码放在一个大文件中。在 ES6 支持模块之前,有两个流行的模块。
-
CommonJS-Node.js
-
AMD(异步模块定义)-浏览器
基本上,使用模块的方式很简单,import
用于从另一个文件中获取功能或几个功能或值,同时export
用于从文件中公开功能或几个功能或值。
导出
使用 ES5 (CommonJS)
// 使用 ES5 CommonJS - helpers.js
exports.isNull = function (val) {
return val === null;
}
exports.isUndefined = function (val) {
return val === undefined;
}
exports.isNullOrUndefined = function (val) {
return exports.isNull(val) || exports.isUndefined(val);
}
使用 ES6 模块
// 使用 ES6 Modules - helpers.js
export function isNull(val){
return val === null;
}
export function isUndefined(val) {
return val === undefined;
}
export function isNullOrUndefined(val) {
return isNull(val) || isUndefined(val);
}
在另一个文件中导入函数
// 使用 ES5 (CommonJS) - index.js
const helpers = require(‘./helpers.js’); // helpers is an object
const isNull = helpers.isNull;
const isUndefined = helpers.isUndefined;
const isNullOrUndefined = helpers.isNullOrUndefined;
// or if your environment supports Destructuring
const { isNull, isUndefined, isNullOrUndefined } = require(‘./helpers.js’);
// ES6 Modules - index.js
import * as helpers from ‘./helpers.js’; // helpers is an object
// or
import { isNull, isUndefined, isNullOrUndefined as isValid } from ‘./helpers.js’;
// using “as” for renaming named exports
在文件中导出单个功能或默认导出
使用 ES5 (CommonJS)
// 使用 ES5 (CommonJS) - index.js
class Helpers {
static isNull(val) {
return val === null;
}
static isUndefined(val) {
return val === undefined;
}
static isNullOrUndefined(val) {
return this.isNull(val) || this.isUndefined(val);
}
}
module.exports = Helpers;
使用ES6 Modules
// 使用 ES6 Modules - helpers.js
class Helpers {
static isNull(val) {
return val === null;
}
static isUndefined(val) {
return val === undefined;
}
static isNullOrUndefined(val) {
return this.isNull(val) || this.isUndefined(val);
}
}
export default Helpers
从另一个文件导入单个功能
使用ES5 (CommonJS)
// 使用 ES5 (CommonJS) - index.js
const Helpers = require(‘./helpers.js’);
console.log(Helpers.isNull(null));
使用 ES6 Modules
import Helpers from ‘.helpers.js’
console.log(Helpers.isNull(null));
48. 什么是Set
对象,它是如何工作的?
Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
我们可以使用Set
构造函数创建Set
实例。
const set1 = new Set();
const set2 = new Set([“a”,“b”,“c”,“d”,“d”,“e”]);
我们可以使用add
方法向Set
实例中添加一个新值,因为add
方法返回Set
对象,所以我们可以以链式的方式再次使用add
。如果一个值已经存在于Set
对象中,那么它将不再被添加。
set2.add(“f”);
set2.add(“g”).add(“h”).add(“i”).add(“j”).add(“k”).add(“k”);
// 后一个“k”不会被添加到set对象中,因为它已经存在了
我们可以使用has
方法检查Set
实例中是否存在特定的值。
set2.has(“a”) // true
set2.has(“z”) // true
我们可以使用size
属性获得Set
实例的长度。
set2.size // returns 10
可以使用clear
方法删除 Set
中的数据。
set2.clear();
我们可以使用Set
对象来删除数组中重复的元素。
const numbers = [1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 5];
const uniqueNums = […new Set(numbers)]; // [1,2,3,4,5,6,7,8]
49. 什么是回调函数?
回调函数是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段(回调函数)代码。
在JavaScript中函数也是对象的一种,同样对象可以作为参数传递给函数,因此函数也可以作为参数传递给另外一个函数,这个作为参数的函数就是回调函数。
const btnAdd = document.getElementById(‘btnAdd’);
btnAdd.addEventListener(‘click’, function clickCallback(e) {
// do something useless
});
在本例中,我们等待id
为btnAdd
的元素中的click
事件,如果它被单击,则执行clickCallback
函数。回调函数向某些数据或事件添加一些功能。
数组中的reduce
、filter
和map
方法需要一个回调作为参数。回调的一个很好的类比是,当你打电话给某人,如果他们不接,你留下一条消息,你期待他们回调。调用某人或留下消息的行为是事件或数据,回调是你希望稍后发生的操作。
50. Promise 是什么?
Promise 是异步编程的一种解决方案:从语法上讲,promise
是一个对象,从它可以获取异步操作的消息;从本意上讲,它是承诺,承诺它过一段时间会给你一个结果。promise
有三种状态:pending(等待态)
,fulfiled(成功态)
,rejected(失败态)
;状态一旦改变,就不会再变。创造promise
实例后,它会立即执行。
fs.readFile(‘somefile.txt’, function (e, data) {
if (e) {
console.log(e);
}
console.log(data);
});
如果我们在回调内部有另一个异步操作,则此方法存在问题。我们将有一个混乱且不可读的代码。此代码称为“回调地狱”。
// 回调地狱
fs.readFile(‘somefile.txt’, function (e, data) {
//your code here
fs.readdir(‘directory’, function (e, files) {
//your code here
fs.mkdir(‘directory’, function (e) {
//your code here
})
})
})
如果我们在这段代码中使用promise
,它将更易于阅读、理解和维护。
promReadFile(‘file/path’)
.then(data => {
return promReaddir(‘directory’);
})
.then(data => {
return promMkdir(‘directory’);
})
.catch(e => {
console.log(e);
})
promise
有三种不同的状态:
-
pending:初始状态,完成或失败状态的前一个状态
-
fulfilled:操作成功完成
-
rejected:操作失败
pending
状态的 Promise
对象会触发 fulfilled/rejected
状态,在其状态处理方法中可以传入参数/失败信息。当操作成功完成时,Promise 对象的 then
方法就会被调用;否则就会触发 catch
。如:
const myFirstPromise = new Promise((resolve, reject) => {
setTimeout(function(){
resolve(“成功!”);
}, 250);
});
myFirstPromise.then((data) => {
console.log("Yay! " + data);
}).catch((e) => {…});
51. 什么是 async/await
及其如何工作?
async/await
是 JS 中编写异步或非阻塞代码的新方法。它建立在Promises之上,让异步代码的可读性和简洁度都更高。
async/await
是 JS 中编写异步或非阻塞代码的新方法。它建立在Promises
之上,相对于 Promise 和回调,它的可读性和简洁度都更高。但是,在使用此功能之前,我们必须先学习Promises
的基础知识,因为正如我之前所说,它是基于Promise
构建的,这意味着幕后使用仍然是Promise。
使用 Promise
function callApi() {
return fetch(“url/to/api/endpoint”)
.then(resp => resp.json())
.then(data => {
//do something with “data”
}).catch(err => {
//do something with “err”
});
}
使用async/await
在async/await
,我们使用 tru/catch 语法来捕获异常。
async function callApi() {
try {
const resp = await fetch(“url/to/api/endpoint”);
const data = await resp.json();
//do something with “data”
} catch (e) {
//do something with “err”
}
}
注意:使用 async
关键声明函数会隐式返回一个Promise。
const giveMeOne = async () => 1;
giveMeOne()
.then((num) => {
console.log(num); // logs 1
});
注意:await
关键字只能在async function
中使用。在任何非async function的函数中使用await
关键字都会抛出错误。await
关键字在执行下一行代码之前等待右侧表达式(可能是一个Promise)返回。
const giveMeOne = async () => 1;
function getOne() {
try {
const num = await giveMeOne();
console.log(num);
} catch (e) {
console.log(e);
}
}
// Uncaught SyntaxError: await is only valid in async function
async function getTwo() {
try {
const num1 = await giveMeOne(); // 这行会等待右侧表达式执行完成
const num2 = await giveMeOne();
return num1 + num2;
} catch (e) {
console.log(e);
}
}
await getTwo(); // 2
52. 展开(spread )运算符和 剩余(Rest) 运算符有什么区别?
展开运算符(spread)是三个点(...
),可以将一个数组转为用逗号分隔的参数序列。说的通俗易懂点,有点像化骨绵掌,把一个大元素给打散成一个个单独的小元素。
剩余运算符也是用三个点(...
)表示,它的样子看起来和展开操作符一样,但是它是用于解构数组和对象。在某种程度上,剩余元素和展开元素相反,展开元素会“展开”数组变成多个元素,剩余元素会收集多个元素和“压缩”成一个单一的元素。
function add(a, b) {
return a + b;
};
const nums = [5, 6];
const sum = add(…nums);
console.log(sum);
在本例中,我们在调用add
函数时使用了展开操作符,对nums
数组进行展开。所以参数a
的值是5
,b
的值是6
,所以sum
是11
。
function add(…rest) {
return rest.reduce((total,current) => total + current);
};
console.log(add(1, 2)); // 3
console.log(add(1, 2, 3, 4, 5)); // 15
在本例中,我们有一个add
函数,它接受任意数量的参数,并将它们全部相加,然后返回总数。
const [first, …others] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(others); // [2,3,4,5]
这里,我们使用剩余操作符提取所有剩余的数组值,并将它们放入除第一项之外的其他数组中。
53. 什么是默认参数?
默认参数是在 JS 中定义默认变量的一种新方法,它在ES6或ECMAScript 2015版本中可用。
//ES5 Version
function add(a,b){
a = a || 0;
b = b || 0;
return a + b;
}
//ES6 Version
function add(a = 0, b = 0){
return a + b;
}
add(1); // returns 1
我们还可以在默认参数中使用解构。
function getFirst([first, …rest] = [0, 1]) {
return first;
}
getFirst(); // 0
getFirst([10,20,30]); // 10
function getArr({ nums } = { nums: [1, 2, 3, 4] }){
return nums;
}
getArr(); // [1, 2, 3, 4]
getArr({nums:[5,4,3,2,1]}); // [5,4,3,2,1]
我们还可以使用先定义的参数再定义它们之后的参数。
function doSomethingWithValue(value = “Hello World”, callback = () => { console.log(value) }) {
callback();
}
doSomethingWithValue(); //“Hello World”
54. 什么是包装对象(wrapper object)?
我们现在复习一下JS的数据类型,JS数据类型被分为两大类,基本类型和引用类型。
基本类型:Undefined
,Null
,Boolean
,Number
,String
,Symbol
,BigInt
引用类型:Object
,Array
,Date
,RegExp
等,说白了就是对象。
其中引用类型有方法和属性,但是基本类型是没有的,但我们经常会看到下面的代码:
let name = “marko”;
console.log(typeof name); // “string”
console.log(name.toUpperCase()); // “MARKO”
name
类型是 string
,属于基本类型,所以它没有属性和方法,但是在这个例子中,我们调用了一个toUpperCase()
方法,它不会抛出错误,还返回了对象的变量值。
原因是基本类型的值被临时转换或强制转换为对象,因此name
变量的行为类似于对象。除null
和undefined
之外的每个基本类型都有自己包装对象。也就是:String
,Number
,Boolean
,Symbol
和BigInt
。在这种情况下,name.toUpperCase()
在幕后看起来如下:
console.log(new String(name).toUpperCase()); // “MARKO”
在完成访问属性或调用方法之后,新创建的对象将立即被丢弃。
55. 隐式和显式转换有什么区别)?
隐式强制转换是一种将值转换为另一种类型的方法,这个过程是自动完成的,无需我们手动操作。
假设我们下面有一个例子。
console.log(1 + ‘6’); // 16
console.log(false + true); // 1
console.log(6 * ‘2’); // 12
第一个console.log
语句结果为16
。在其他语言中,这会抛出编译时错误,但在 JS 中,1
被转换成字符串,然后与+运
算符连接。我们没有做任何事情,它是由 JS 自动完成。
第二个console.log
语句结果为1
,JS 将false
转换为boolean
值为 0
,,true
为1
,因此结果为1
。
第三个console.log
语句结果12
,它将'2'
转换为一个数字,然后乘以6 * 2
,结果是12。
而显式强制是将值转换为另一种类型的方法,我们需要手动转换。
console.log(1 + parseInt(‘6’));
在本例中,我们使用parseInt
函数将'6'
转换为number
,然后使用+
运算符将1
和6
相加。
56. 什么是NaN?以及如何检查值是否为NaN?
NaN
表示“非数字”是 JS 中的一个值,该值是将数字转换或执行为非数字值的运算结果,因此结果为NaN
。
let a;
console.log(parseInt(‘abc’)); // NaN
console.log(parseInt(null)); // NaN
console.log(parseInt(undefined)); // NaN
console.log(parseInt(++a)); // NaN
console.log(parseInt({} * 10)); // NaN
console.log(parseInt(‘abc’ - 2)); // NaN
console.log(parseInt(0 / 0)); // NaN
console.log(parseInt(‘10a’ * 10)); // NaN
JS 有一个内置的isNaN
方法,用于测试值是否为isNaN值,但是这个函数有一个奇怪的行为。
console.log(isNaN()); // true
console.log(isNaN(undefined)); // true
console.log(isNaN({})); // true
console.log(isNaN(String(‘a’))); // true
console.log(isNaN(() => { })); // true
所有这些console.log
语句都返回true
,即使我们传递的值不是NaN
。
在ES6
中,建议使用Number.isNaN
方法,因为它确实会检查该值(如果确实是NaN
),或者我们可以使自己的辅助函数检查此问题,因为在 JS 中,NaN是唯一的值,它不等于自己。
function checkIfNaN(value) {
return value !== value;
}
57. 如何判断值是否为数组?
我们可以使用Array.isArray
方法来检查值是否为数组。当传递给它的参数是数组时,它返回true
,否则返回false
。
console.log(Array.isArray(5)); // false
console.log(Array.isArray(“”)); // false
console.log(Array.isArray()); // false
console.log(Array.isArray(null)); // false
console.log(Array.isArray({ length: 5 })); // false
console.log(Array.isArray([])); // true
如果环境不支持此方法,则可以使用polyfill
实现。
function isArray(value){
return Object.prototype.toString.call(value) === “[object Array]”
}
当然还可以使用传统的方法:
let a = []
if (a instanceof Array) {
console.log(‘是数组’)
} else {
console.log(‘非数组’)
}
58. 如何在不使用%
模运算符的情况下检查一个数字是否是偶数?
我们可以对这个问题使用按位&
运算符,&
对其操作数进行运算,并将其视为二进制值,然后执行与运算。
function isEven(num) {
if (num & 1) {
return false
} else {
return true
}
}
0
二进制数是 000
1
二进制数是 001
2
二进制数是 010
3
二进制数是 011
4
二进制数是 100
5
二进制数是 101
6
二进制数是 110
7
二进制数是 111
以此类推…
与运算的规则如下:
| a | b | a & b |
| :-- | :-- | :-- |
| 0 | 0 | 0 |
| 0 | 1 | 0 |
| 1 | 1 | 1 |
因此,当我们执行console.log(5&1)
这个表达式时,结果为1
。首先,&
运算符将两个数字都转换为二进制,因此5
变为101
,1
变为001
。
然后,它使用按位怀运算符比较每个位(0
和1
)。 101&001
,从表中可以看出,如果a & b
为1
,所以5&1
结果为1
。
| 101 & 001 |
| — |
| 101 |
| 001 |
| 001 |
-
首先我们比较最左边的
1&0
,结果是0
。 -
然后我们比较中间的
0&0
,结果是0
。 -
然后我们比较最后
1&1
,结果是1
。 -
最后,得到一个二进制数
001
,对应的十进制数,即1
。
由此我们也可以算出console.log(4 & 1)
结果为0
。知道4
的最后一位是0
,而0 & 1
将是0
。如果你很难理解这一点,我们可以使用递归函数来解决此问题。
function isEven(num) {
if (num < 0 || num === 1) return false;
if (num == 0) return true;
return isEven(num - 2);
}
59. 如何检查对象中是否存在某个属性?
检查对象中是否存在属性有三种方法。
第一种使用 in
操作符号:
const o = {
“prop” : “bwahahah”,
“prop2” : “hweasa”
};
console.log(“prop” in o); // true
console.log(“prop1” in o); // false
第二种使用 hasOwnProperty
方法,hasOwnProperty()
方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。
console.log(o.hasOwnProperty(“prop2”)); // true
console.log(o.hasOwnProperty(“prop1”)); // false
第三种使用括号符号obj["prop"]
。如果属性存在,它将返回该属性的值,否则将返回undefined
。
console.log(o[“prop”]); // “bwahahah”
console.log(o[“prop1”]); // undefined
60. AJAX 是什么?
即异步的 JavaScript 和 XML,是一种用于创建快速动态网页的技术,传统的网页(不使用 AJAX)如果需要更新内容,必需重载整个网页面。使用AJAX则不需要加载更新整个网页,实现部分内容更新
用到AJAX的技术:
-
HTML - 网页结构
-
CSS - 网页的样式
-
JavaScript - 操作网页的行为和更新DOM
-
XMLHttpRequest API - 用于从服务器发送和获取数据
-
PHP,Python,Nodejs - 某些服务器端语言
61. 如何在 JS 中创建对象?
使用对象字面量:
const o = {
name: “Mark”,
greeting() {
return Hi, I'm ${this.name}
;
}
};
o.greeting(); //returns “Hi, I’m Mark”
使用构造函数:
function Person(name) {
this.name = name;
}
Person.prototype.greeting = function () {
return Hi, I'm ${this.name}
;
}
const mark = new Person(“Mark”);
mark.greeting(); //returns “Hi, I’m Mark”
使用 Object.create 方法:
const n = {
greeting() {
return Hi, I'm ${this.name}
;
}
};
const o = Object.create(n); // sets the prototype of “o” to be “n”
o.name = “Mark”;
console.log(o.greeting()); // logs “Hi, I’m Mark”
62. Object.seal 和 Object.freeze 方法之间有什么区别?
这两种方法之间的区别在于,当我们对一个对象使用Object.freeze方法时,该对象的属性是不可变的,这意味着我们不能更改或编辑这些属性的值。而在Obj.Engor方法中,我们可以改变现有的属性。
Object.freeze()
Object.freeze()
方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze()
返回和传入的参数相同的对象。
Object.seal()
Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。
方法的相同点:
-
ES5新增。
-
对象不可能扩展,也就是不能再添加新的属性或者方法。
-
对象已有属性不允许被删除。
-
对象属性特性不可以重新配置。
方法不同点:
-
Object.seal
方法生成的密封对象,如果属性是可写的,那么可以修改属性值。 -
Object.freeze
方法生成的冻结对象,属性都是不可写的,也就是属性值无法更改。
63. in
运算符和 Object.hasOwnProperty
方法有什么区别?
如你所知,这两个特性都检查对象中是否存在属性,它将返回truefalse。它们之间的区别在于,in操作符还会检查对象的原型链,如果属性在当前对象中没有找到,而hasOwnProperty方法只检查属性是否存在于当前对象中,而忽略原型链。
hasOwnPropert方法
hasOwnPropert()
方法返回值是一个布尔值,指示对象自身属性中是否具有指定的属性,因此这个方法会忽略掉那些从原型链上继承到的属性。
看下面的例子:
Object.prototype.phone= ‘15345025546’;
let obj = {
name: ‘西门大官人’,
age: ‘28’
}
console.log(obj.hasOwnProperty(‘phone’)) // false
console.log(obj.hasOwnProperty(‘name’)) // true
可以看到,如果在函数原型上定义一个变量phone
,hasOwnProperty
方法会直接忽略掉。
in 运算符
如果指定的属性在指定的对象或其原型链中,则in
运算符返回true
。
还是用上面的例子来演示:
console.log(‘phone’ in obj) // true
可以看到in
运算符会检查它或者其原型链是否包含具有指定名称的属性。
64. 有哪些方法可以处理 JS 中的异步代码?
-
回调
-
Promise
-
async/await
-
还有一些库:async.js, bluebird, q, co
65. 函数表达式和函数声明之间有什么区别?
看下面的例子:
前端框架
前端框架太多了,真的学不动了,别慌,其实对于前端的三大马车,Angular、React、Vue 只要把其中一种框架学明白,底层原理实现,其他两个学起来不会很吃力,这也取决于你以后就职的公司要求你会哪一个框架了,当然,会的越多越好,但是往往每个人的时间是有限的,对于自学的学生,或者即将面试找工作的人,当然要选择一门框架深挖原理。
以 Vue 为例,我整理了如下的面试题。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注前端)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
); // undefined
60. AJAX 是什么?
即异步的 JavaScript 和 XML,是一种用于创建快速动态网页的技术,传统的网页(不使用 AJAX)如果需要更新内容,必需重载整个网页面。使用AJAX则不需要加载更新整个网页,实现部分内容更新
用到AJAX的技术:
-
HTML - 网页结构
-
CSS - 网页的样式
-
JavaScript - 操作网页的行为和更新DOM
-
XMLHttpRequest API - 用于从服务器发送和获取数据
-
PHP,Python,Nodejs - 某些服务器端语言
61. 如何在 JS 中创建对象?
使用对象字面量:
const o = {
name: “Mark”,
greeting() {
return Hi, I'm ${this.name}
;
}
};
o.greeting(); //returns “Hi, I’m Mark”
使用构造函数:
function Person(name) {
this.name = name;
}
Person.prototype.greeting = function () {
return Hi, I'm ${this.name}
;
}
const mark = new Person(“Mark”);
mark.greeting(); //returns “Hi, I’m Mark”
使用 Object.create 方法:
const n = {
greeting() {
return Hi, I'm ${this.name}
;
}
};
const o = Object.create(n); // sets the prototype of “o” to be “n”
o.name = “Mark”;
console.log(o.greeting()); // logs “Hi, I’m Mark”
62. Object.seal 和 Object.freeze 方法之间有什么区别?
这两种方法之间的区别在于,当我们对一个对象使用Object.freeze方法时,该对象的属性是不可变的,这意味着我们不能更改或编辑这些属性的值。而在Obj.Engor方法中,我们可以改变现有的属性。
Object.freeze()
Object.freeze()
方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze()
返回和传入的参数相同的对象。
Object.seal()
Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。
方法的相同点:
-
ES5新增。
-
对象不可能扩展,也就是不能再添加新的属性或者方法。
-
对象已有属性不允许被删除。
-
对象属性特性不可以重新配置。
方法不同点:
-
Object.seal
方法生成的密封对象,如果属性是可写的,那么可以修改属性值。 -
Object.freeze
方法生成的冻结对象,属性都是不可写的,也就是属性值无法更改。
63. in
运算符和 Object.hasOwnProperty
方法有什么区别?
如你所知,这两个特性都检查对象中是否存在属性,它将返回truefalse。它们之间的区别在于,in操作符还会检查对象的原型链,如果属性在当前对象中没有找到,而hasOwnProperty方法只检查属性是否存在于当前对象中,而忽略原型链。
hasOwnPropert方法
hasOwnPropert()
方法返回值是一个布尔值,指示对象自身属性中是否具有指定的属性,因此这个方法会忽略掉那些从原型链上继承到的属性。
看下面的例子:
Object.prototype.phone= ‘15345025546’;
let obj = {
name: ‘西门大官人’,
age: ‘28’
}
console.log(obj.hasOwnProperty(‘phone’)) // false
console.log(obj.hasOwnProperty(‘name’)) // true
可以看到,如果在函数原型上定义一个变量phone
,hasOwnProperty
方法会直接忽略掉。
in 运算符
如果指定的属性在指定的对象或其原型链中,则in
运算符返回true
。
还是用上面的例子来演示:
console.log(‘phone’ in obj) // true
可以看到in
运算符会检查它或者其原型链是否包含具有指定名称的属性。
64. 有哪些方法可以处理 JS 中的异步代码?
-
回调
-
Promise
-
async/await
-
还有一些库:async.js, bluebird, q, co
65. 函数表达式和函数声明之间有什么区别?
看下面的例子:
前端框架
前端框架太多了,真的学不动了,别慌,其实对于前端的三大马车,Angular、React、Vue 只要把其中一种框架学明白,底层原理实现,其他两个学起来不会很吃力,这也取决于你以后就职的公司要求你会哪一个框架了,当然,会的越多越好,但是往往每个人的时间是有限的,对于自学的学生,或者即将面试找工作的人,当然要选择一门框架深挖原理。
以 Vue 为例,我整理了如下的面试题。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加V获取:vip1024c (备注前端)
[外链图片转存中…(img-FAsE8Svw-1713280259409)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!