近期总结了一些前端基础的知识,都是较为高频的前端面试题,尽量解答完整详细,并分享给大家一起学习。如有问题,欢迎指正。
1. 原型链
- 每个构造函数都有一个显式原型prototype
- 每个对象都有一个隐式原型_proto_
- 对象的_proto_指向其构造函数的prototype,
- 使用对象的某个属性时,如果没有这个属性,会去该对象构造函数的prototype中去找,直到object.prototype(指向null)
2. 闭包
- 概念:当一个函数能使用另外一个函数中的变量时,就构成了一个闭包,内部函数可以访问在外部函数定义的变量,而外部函数的作用域链会被保留在闭包中,即使外部函数已经执行完毕。
function outer() {
var count = 0;
function inner() {
count++;
console.log(count);
}
return inner;
}
var counter = outer();
counter(); // 输出 1
counter(); // 输出 2
counter(); // 输出 3
- 产生:
- 在一个函数中嵌套另一个函数
- 将一个匿名函数作为值传入另外一个函数中,即函数当作参数传递
- 应用场景:
- 模块化
使用闭包可以实现模块化,将变量和方法封装在一个函数内部,外部无法直接访问,只能通过暴露的接口访问。
var module = (function() {
var count = 0;
function increment() {
count++;
}
function getCount() {
return count;
}
return {
increment: increment,
getCount: getCount
};
})();
module.increment();
module.increment();
console.log(module.getCount()); // 输出 2
2.私有变量
使用闭包可以实现私有变量,将变量定义在函数内部,外部无法访问。
function Counter() {
var count = 0;
this.increment = function() {
count++;
};
this.getCount = function() {
return count;
};
}
var counter = new Counter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 输出 2
- 函数柯里化
函数柯里化又称部分求值,是指将多个参数的函数转换成一系列只有一个参数的函数的过程,就是将:function(arg1,arg2,…,argn)变成 function(arg1)(arg2)...(argn),该函数不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来,多次调用,直到之前传入的所有参数都会被一次性用于求值。使用闭包可以方便地实现函数柯里化。
柯里化函数:
function add(a, b) {
return a + b;
}
function curry(fn) {
return function(a) {
return function(b) {
return fn(a, b);
};
};
}
var addCurry = curry(add);
console.log(addCurry(1)(2)); // 输出 3
- 缺点:因为闭包引用着另一个函数的变量,导致另一个函数已经不使用了也无法销毁,所以闭包使用过多,会占用较多的内存,造成内存泄漏。
- 销毁:把被引用的变量设置为null,即手动清除变量
3. this
JavaScript 中的 this 关键字是一个非常重要的概念,它用于引用当前函数执行上下文中的对象。this 的值根据执行上下文的不同而变化,因此理解 this 的工作原理对于编写高质量的 JavaScript 代码非常重要。
this 的值主要分为以下四种:
- 全局上下文
在全局上下文中,this 的值等于全局对象(在浏览器中是 window 对象,在 Node.js 中是 global 对象)。
- 函数上下文
在函数上下文中,this 的值取决于函数的调用方式。如果函数是作为对象的方法调用,则 this 的值等于该对象;如果函数是作为普通函数调用,则 this 的值等于全局对象。
const obj = {
name: 'John',
sayName() {
console.log(this.name);
}
};
obj.sayName(); // 输出 "John"
const sayName = obj.sayName;
sayName(); // 输出 "undefined"(在浏览器中是 "window")
- 构造函数上下文
在构造函数中,this 的值等于正在构造的新对象。构造函数中的 this 关键字通常与 new 运算符一起使用。
- 显式设置
this
call 和 apply 方法会立即调用函数,且将函数的第一个参数作为 this 的值。
可以使用 call、apply 和 bind 方法显式设置函数的 this 值。
//function.call(obj,arg1,arg2,arg3…) 后面的参数为列表项
//function.apply(obj,argArray) argArray为[数组]
function sayName() {
console.log(this.name);
}
const obj1 = { name: 'John' };
const obj2 = { name: 'Jane' };
sayName.call(obj1); // 输出 "Jane"
sayName.apply(obj2); //输出 "John"
bind 方法不会立即调用函数,而是返回一个新的函数,将第一个参数作为 this 的值。新函数可以在之后的任何时候调用。
//function.bind(obj,arg1,arg2,arg3…) 后面的参数为列表项
function sayName() {
console.log(this.name);
}
const obj1 = { name: 'John' };
const sayName1 = sayName.bind(obj1);
sayName1(); // 输出 "John"
4. 箭头函数
- 本身没有this、super、new.target,函数内this指向外层的第一个普通函数的this或全局对象,this不可变
- 本身没有arguments(可用...扩展符来获取)
- 不能当作构造函数:由于箭头函数没有自己的
this,也不能使用new运算符调用,因此不能用作构造函数 - 不支持重复命名的参数
- 不能用作生成器函数:箭头函数不能用作生成器函数,因为它们没有自己的
yield关键字。
5.继承
- 构造函数继承:
构造函数继承是指在子类的构造函数中调用父类的构造函数,并使用 call 或 apply 方法将父类的属性绑定到子类上
优点:简单易懂,容易理解和实现
缺点:无法继承父类原型上的方法和属性,而且无法实现多层继承
function Animal(name) {
this.name = name;
}
function Cat(name, breed) {
Animal.call(this, name); // 使用 call 方法调用父类构造函数
this.breed = breed;
}
var myCat = new Cat('Buddy', 'Golden Retriever');
console.log(myCat.name); // 输出 "Buddy"
console.log(myCat.breed); // 输出 "Golden Retriever"
- 原型链继承:
原型链继承是指子类的原型对象通过 Object.create 方法创建,并将父类的原型对象作为参数传入。这样,子类的原型对象就继承了父类的原型对象的所有属性和方法
优点:能够继承父类原型上的方法和属性,可以实现多层继承
缺点:无法继承父类构造函数中的属性,而且所有子类对象共享父类原型对象,容易造成属性污染
function Animal(name) {
this.name = name;
}
Animal.prototype.sayName = function() {
console.log(this.name);
}
function Cat(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Cat.prototype = Object.create(Animal.prototype); // 使用 Object.create 方法创建子类原型对象
Cat.prototype.constructor = Cat;
var myCat = new Cat('Buddy', 'Golden Retriever');
myCat.sayName(); // 输出 "Buddy"
- 组合继承:
组合继承是指将构造函数继承和原型链继承结合起来,既能继承父类构造函数中的属性,也能继承父类原型对象上的方法和属性。
优点:能够继承父类构造函数中的属性和父类原型上的方法和属性,比较完整地实现了继承
缺点:是在创建子类对象时,会调用两次父类构造函数,其中一次在子类原型对象的创建过程中。这会造成不必要的开销,降低了性能。
function Animal(name) {
this.name = name;
}
Animal.prototype.sayName = function() {
console.log(this.name);
}
function Cat(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Cat.prototype = new Animal(); // 继承父类原型对象
Cat.prototype.constructor = Cat;
var myCat = new Cat('Buddy', 'Golden Retriever');
myCat.sayName(); // 输出 "Buddy"
- 寄生组合式继承:
寄生组合式继承是指在组合继承的基础上,通过一些技巧解决调用两次父类构造函数的问题。其背后的基本思路是:不必为了子类型的原型而调用父类型的构造函数,我们所需的无非是要父类型原型的一个副本而已。本质上就是使用寄生式继承来继承父类型的原型,然后再将结果指定给子类型的原型。
寄生组合式继承的核心思路是其实就是换一种方式实现Cat.prototype = new Animal();
优点:能够继承父类构造函数中的属性和父类原型上的方法和属性,比较完整地实现了继承。而且不会调用两次父类构造函数,避免了性能问题。
缺点:相对于组合继承,需要使用一些比较复杂的技巧实现。
function Animal(name) {
this.name = name;
}
Animal.prototype.sayName = function() {
console.log(this.name);
}
function Cat(name, breed) {
// 继承父类构造函数中的属性
Animal.call(this, name);
this.breed = breed;
}
// 创建一个新对象,将其原型对象指向父类原型对象的副本
Cat.prototype = Object.create(Animal.prototype);
// 增强对象,弥补因重写原型而失去的默认的constructor 属性
Cat.prototype.constructor = Cat;
var myCat = new Cat('Buddy', 'Golden Retriever');
myCat.sayName(); // 输出 "Buddy"
6. 什么是跨域?如何解决跨域问题?
跨域指的是在同一页面中,不同域名(或端口号、协议)之间的请求。出于安全原因,浏览器限制脚本从一个源加载另一个源的资源。解决跨域问题的方法有以下几种:
- JSONP:通过动态创建 script 标签,将需要获取的数据作为回调参数传递,并由服务端返回 JavaScript 函数调用,从而实现数据获取。但是,JSONP 只支持 GET 请求,并且不安全,容易被注入攻击。
- CORS:跨源资源共享(Cross-Origin Resource Sharing),是一种允许在浏览器中访问不同源的资源的机制。在服务端设置响应头 Access-Control-Allow-Origin,即可允许跨域请求。
- 代理(nginx代理跨域,nodejs中间件代理跨域):将前端请求发送给后端,由后端进行跨域请求,并将结果返回给前端。这种方法需要后端进行额外的开发和维护,并且会增加服务器负担
- postMessage
- WebSocket
7. 什么是事件委托?如何实现事件委托?
事件委托是一种优化 JavaScript 事件监听器的技术。它利用事件冒泡机制,将事件处理程序添加到父元素,以便处理所有子元素上的事件。通过事件委托,我们可以避免将事件处理程序附加到每个子元素上,从而提高性能。实现事件委托的代码如下:
const parentElement = document.querySelector('#parent-element');
parentElement.addEventListener('click', function(event) {
if (event.target.matches('.child-element')) {
// 处理子元素的点击事件
}
});
缺点:存在安全问题:如果事件代理的范围过大,可能会导致安全问题,例如通过点击链接触发事件代理,从而执行恶意代码。
8. 事件循环(Event Loop)
在 JavaScript 中,所有的代码都是以事件的形式被执行的,事件循环就是负责管理和调度这些事件的机制。
事件循环的执行流程如下:
- 执行同步任务,直到调用栈(执行上下文栈)为空。
- 执行微任务队列中的所有任务,直到微任务队列为空。微任务队列中的任务可以是 Promise 的回调函数等。
虽然 MutationObserver(Web API)的回调函数 和 process.nextTick (node)中 方法也可以产生微任务,但它们只在特定的环境中使用,并不是标准的微任务产生方式。在大多数情况下,我们只需要关注 Promise 的回调函数即可。
- 从宏任务队列中取出第一个任务,执行它的回调函数。
宏任务:
1. setTimeout() 和 setInterval()
2. I/O 操作
在 Node.js 环境中,执行 I/O 操作(例如读取文件、发送网络请求等)时,会产生一个宏任务,并将回调函数添加到宏任务队列中。当 I/O 操作完成时,JavaScript 引擎会从宏任务队列中取出该任务,并执行回调函数。
3. UI 交互事件
在浏览器环境中,用户的 UI 交互事件(例如鼠标点击、滚动、键盘输入等)会产生一个宏任务,并将回调函数添加到宏任务队列中。当浏览器空闲时,JavaScript 引擎会从宏任务队列中取出该任务,并执行回调函数。
4.postMessage
使用 postMessage() 方法可以向指定的窗口或子框架发送消息,并在接收到消息后产生一个宏任务,并将回调函数添加到宏任务队列中。
- 如果当前宏任务产生了新的微任务,将这些微任务添加到微任务队列的末尾。
- 重复步骤 2-4,直到任务队列为空。
9. 浅拷贝和深拷贝
浅拷贝: 自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果属性是基本类型(如字符串、数字、布尔值等),则复制的是基本类型的值;如果属性是引用类型(如对象、数组等),则复制的是引用类型的地址,两个对象会共享同一个内存地址。
浅拷贝的方法有:
- Object.assign()方法
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = Object.assign({}, obj1);
console.log(obj2); // { a: 1, b: { c: 2 } }
- 扩展运算符
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = { ...obj1 };
console.log(obj2); // { a: 1, b: { c: 2 } }
深拷贝:将一个对象的属性值复制到另一个对象,并且对于引用类型的属性,也进行递归复制,使得复制的对象与原对象完全独立,两个对象拥有不同的内存地址。
深拷贝的方法有:
- JSON.parse(JSON.stringify())方法
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2); // { a: 1, b: { c: 2 } }
注意:使用该方法时要注意对象中不能含有循环引用(即对象中某个属性指向该对象本身)或者函数属性,否则会出现问题。
- 递归复制
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
let cloneObj = Array.isArray(obj) ? [] : {};
for (let key in obj) {
cloneObj[key] = deepClone(obj[key]);
}
return cloneObj;
}
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = deepClone(obj1);
console.log(obj2); // { a: 1, b: { c: 2 } }