ldt - 20231225部门考核题
什么是闭包,并举例说明。
函数可以访问和操作其方法作用域之外的变量的作用域链。例如,在一个函数内部定义另一个函数,并返回这个函数,这个函数就可以访问其父级函数的变量,即使父级函数已经执行完毕。
问题:请解释一下什么是前端中的JavaScript中的闭包(Closure)及其作用和使用场景?
闭包是一种函数,它可以在其内部定义并访问其他函数的作用域变量。闭包的作用包括实现私有变量、封装功能模块、实现回调函数等。它的使用场景包括创建可复用的函数组件、实现异步操作、创建工厂函数等。
闭包的理解
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包的特点
- 让外部访问函数内部变量变成可能
- 变量会常驻在内存中
- 可以避免使用全局变量,防止全局变量污染;
优劣
好处:可以读取其他函数内部的变量,并将其一直保存在内存中。
坏处:可能会造成内存泄漏或溢出。
两个常用的用途
创建私有变量
闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
变量常驻内存
闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
eg
闭包函数
比如,函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。
function A() {
let a = 1
window.B = function () {
console.log(a)
}
}
A()
B() // 1
循环中使用闭包解决 var 定义函数
在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。经典面试题:循环中使用闭包解决 var 定义函数的问题
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。解决办法有三种:
- 第一种是使用闭包的方式
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j)
}, j * 1000)
})(i)
}
在上述代码中,首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。
- 第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。
for (var i = 1; i <= 5; i++) {
setTimeout(
function timer(j) {
console.log(j)
},
i * 1000,
i
)
}
- 第三种就是使用 let 定义 i 了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i)
}, i * 1000)
}
注意点
- 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
ES6的新特性,它们的特点和使用场景
ES6引入了许多新特性,如let和const关键字、箭头函数、模板字符串、解构赋值、Promise等。这些特性提供了更简洁、更强大的语法和功能,可以帮助开发人员更高效地编写JavaScript代码。例如,let和const关键字提供了块级作用域和不可变变量的功能,可以避免一些常见的JavaScript陷阱。箭头函数提供了更简洁的函数语法和自动绑定this的特性,可以使代码更易于阅读和维护。
变量声明:const和let
ES6推荐使用let声明局部变量,相比之前的var(无论声明在何处,都会被视为声明在函数的最顶部),let具有块级作用域(即在使用let的那一对大括号 {}内起作用),可以避免变量提升和污染全局作用域的问题。
const则用来声明常量,即一旦赋值就不能再改变。const也具有块级作用域,并且必须在声明时就初始化。const适合用来定义不会改变的值,如PI、URL等。
// 使用var
var a = 1;
if (true) {
var a = 2; // 这里会覆盖上面的a
}
console.log(a); // 输出2
// 使用let
let b = 1;
if (true) {
let b = 2; // 这里不会影响外面的b
}
console.log(b); // 输出1
// 使用const
const PI = 3.14;
PI = 3.15; // 报错:Assignment to constant variable.
const URL = "https://www.bing.com";
URL = "https://www.google.com"; // 报错:Assignment to constant variable.
解构赋值
解构赋值是一种从数组或对象中提取数据并赋值给变量的简洁写法。它可以减少代码量,提高可读性,并且支持默认值、嵌套结构、别名等特性。
// 数组解构:按照数组元素位置对应赋值给变量
let [f, g, h] = [1, 2, 3];
console.log(f); // 1
console.log(g); // 2
console.log(h); // 3
// 对象解构:按照对象属性名对应赋值给同名变量(也可以使用别名)
let {name, age} = {name: "Alice", age: 18};
console.log(name); // Alice
console.log(age); // 18
let {name: i, age: j} = {name: "Bob", age: 19};
console.log(i); // Bob
console.log(j); //19
// 默认值:如果没有匹配到相应的数据,则使用默认值(如果有)
let [k, l=0] = [1];
console.log(k); //1
console.log(l); //0
let {m=0, n=0} = {m:1};
console.log(m); //1
console.log(n); //0
// 嵌套结构:可以解构嵌套的数组或对象
let [o, [p, q]] = [1, [2, 3]];
console.log(o); // 1
console.log(p); // 2
console.log(q); // 3
let {r: {s, t}} = {r: {s: 4, t: 5}};
console.log(s); // 4
console.log(t); // 5
// 解构赋值的应用场景:交换变量、函数参数、返回值等
let u = 6;
let v = 7;
[u, v] = [v, u]; // 不需要使用临时变量来交换u和v的值
console.log(u); // 7
console.log(v); // 6
function foo([x, y]) {
return x + y;
}
console.log(foo([8,9])); //17
function bar() {
return [10,11];
}
let [w,z] = bar();
console.log(w); //10
console.log(z); //11
箭头函数
箭头函数是一种使用 => 符号定义函数的简洁写法 。它可以省略 function 关键字、参数括号、返回值括号等,使得代码更加简洁和清晰。箭头函数还有一个重要的特性,就是它不会改变 this 的指向,即箭头函数内部的 this 始终指向定义时所在的对象。
// 普通函数和箭头函数的对比
function add(x,y) {
return x + y;
}
let add = (x,y) => x + y; // 省略了 function 关键字、参数括号、返回值括号
// 如果只有一个参数,可以省略参数括号;如果没有参数,必须使用空括号
let square = x => x * x;
let hello = () => console.log("Hello");
// 如果有多条语句,需要使用大括号包裹,并且显式返回(如果需要)
let max = (x,y) => {
if (x > y) {
return x;
} else {
return y;
}
};
// 箭头函数不会改变 this 的指向,即箭头函数内部的 this 始终指向定义时所在的对象
let obj = {
name: "Alice",
sayHi: function() {
console.log(this.name); // Alice
setTimeout(function() {
console.log(this.name); // undefined (因为setTimeout中的this指向window)
},1000);
setTimeout(() => {
console.log(this.name); // Alice (因为箭头函数中的this指向obj)
},1000);
}
};
obj.sayHi();
模板字符串
模板字符串是一种使用反引号 `` 包裹字符串,并且支持插入变量或表达式的新语法 。它可以避免使用 + 号连接字符串和变量,并且支持多行字符串和标签模板等特性。
// 使用模板字符串插入变量或表达式,用 ${} 包裹即可(注意是反引号而不是单引号)
let name = "Bob";
let age = 19;
let message = `Hello ${name}, you are ${age} years old.`;
console.log(message);
// 使用模板字符串可以直接换行,不需要使用 \n 或者 + 号连接多行字符串
let poem =
`Do not go gentle into that good night,
Old age should burn and rave at close of day;
Rage, rage against the dying of the light.`;
console.log(poem);
// 使用标签模板可以自定义模板字符串的处理方式(标签是一个函数名)
function tag(strings,...values) {
let result = "";
for (let i=0; i<strings.length; i++) {
result += strings[i];
if (i < values.length) {
result += values[i].toUpperCase(); // 将变量转为大写
}
}
return result;
}
let name = "Alice";
let message = tag`Hello ${name}, how are you?`; // 使用tag函数处理模板字符串
console.log(message); // Hello ALICE, how are you?
默认参数、剩余参数和展开运算符
ES6提供了一些新的语法,可以让函数的参数更加灵活和方便。默认参数可以让函数在没有传入参数或传入undefined时,使用预设的默认值。剩余参数可以让函数接收任意数量的参数,并将它们存储在一个数组中。展开运算符可以将一个数组或对象展开为多个元素或属性,用于函数调用、数组合并、对象复制等场景。
// 默认参数:在函数定义时,给参数赋予默认值(如果没有传入或传入undefined)
function add(x=0,y=0) {
return x + y;
}
console.log(add()); // 0
console.log(add(1)); // 1
console.log(add(1,2)); // 3
console.log(add(1,undefined)); // 1
// 剩余参数:在函数定义时,使用 ... 符号表示剩余的所有参数,并将它们存储在一个数组中(必须是最后一个参数)
function sum(...numbers) {
let result = 0;
for (let number of numbers) {
result += number;
}
return result;
}
console.log(sum()); // 0
console.log(sum(1)); // 1
console.log(sum(1,2)); // 3
console.log(sum(1,2,3)); // 6
// 展开运算符:在函数调用时,使用 ... 符号将一个数组或对象展开为多个元素或属性(相当于逐个传入)
let arr = [4,5,6];
console.log(sum(...arr)); // 15 (相当于sum(4,5,6))
// 展开运算符也可以用于数组合并、对象复制等场景
let arr1 = [1,2];
let arr2 = [3,...arr]; // [3,4,5,6]
let obj1 = {name: "Alice"};
let obj2 = {...obj1}; // {name: "Alice"}
类和继承
ES6提供了一种新的语法,可以让JavaScript支持类和继承这两个面向对象编程的重要概念。类是一种定义对象属性和方法的模板,可以通过 new 关键字创建类的实例。继承是一种让子类拥有父类属性和方法的机制,可以通过 extends 和 super 关键字实现。
// 定义一个类:使用 class 关键字,并且提供一个 constructor 方法作为构造函数(初始化实例属性)
class Person {
constructor(name,age) {
this.name = name; // this 指向实例对象
this.age = age;
}
// 定义类的方法:直接在类中写函数名和函数体(不需要使用 function 关键字)
sayHi() {
console.log(`Hello ${this.name}, you are ${this.age} years old.`);
}
}
// 创建类的实例:使用 new 关键字,并且传入构造函数所需的参数
let alice = new Person("Alice", 18);
alice.sayHi(); // Hello Alice, you are 18 years old.
// 定义一个子类:使用 extends 关键字继承父类,并且可以重写或新增属性和方法
class Student extends Person {
constructor(name, age, grade) {
super(name, age); // 使用 super 关键字调用父类的构造函数(必须在子类构造函数中第一行执行)
this.grade = grade; // 子类可以新增自己的属性
}
// 子类可以重写或新增父类的方法
sayHi() {
console.log(`Hello ${this.name}, you are ${this.age} years old and in grade ${this.grade}.`);
}
study() {
console.log(`${this.name} is studying hard.`);
}
}
// 创建子类的实例:使用 new 关键字,并且传入构造函数所需的参数(包括父类和子类的参数)
let bob = new Student("Bob", 19, 12);
bob.sayHi(); // Hello Bob, you are 19 years old and in grade 12.
bob.study(); // Bob is studying hard.
Promise和async/await
Promise是一种用于处理异步操作的对象,它表示一个未来可能完成或失败的事件。Promise有三种状态:pending(等待)、fulfilled(成功)、rejected(失败)。Promise可以通过 then 方法添加成功或失败时执行的回调函数,也可以通过 catch 方法添加失败时执行的回调函数。Promise还可以通过 all 和 race 方法组合多个Promise对象。
async/await是一种基于Promise的新语法,可以让异步操作更加简洁和清晰。async是一个修饰符,用于声明一个异步函数,该函数返回一个Promise对象。await是一个运算符,用于等待一个Promise对象的结果,只能在异步函数中使用。
// 创建一个Promise对象:使用 new 关键字,并且传入一个执行器函数(该函数接收两个参数:resolve和reject)
let promise = new Promise((resolve,reject) => {
setTimeout(() => { // 模拟一个异步操作
let num = Math.random(); // 随机生成一个0到1之间的数
if (num > 0.5) {
resolve(num); // 如果大于0.5,则表示成功,调用resolve并传入结果
} else {
reject(num); // 如果小于等于0.5,则表示失败,调用reject并传入结果
}
},1000);
});
// 使用 then 方法添加成功或失败时执行的回调函数(可以链式调用)
promise.then((value) => {
console.log(`Success: ${value}`); // 如果Promise状态变为fulfilled,打印成功的结果
return value * 2; // 可以返回一个新的值,传递给下一个then
}).then((value) => {
console.log(`Double: ${value}`); // 打印上一个then返回的值乘以2
}).catch((reason) => {
console.log(`Fail: ${reason}`); // 如果Promise状态变为rejected,打印失败的结果
});
// 使用 Promise.all 方法组合多个Promise对象,返回一个新的Promise对象,该对象在所有Promise都成功时成功,否则失败
let promise1 = Promise.resolve(1); // 创建一个立即成功的Promise对象
let promise2 = Promise.resolve(2);
let promise3 = Promise.resolve(3);
let promise4 = Promise.all([promise1,promise2,promise3]); // 组合三个Promise对象
promise4.then((values) => {
console.log(values); // [1,2,3] (如果所有Promise都成功,打印一个包含所有结果的数组)
}).catch((reason) => {
console.log(reason); // 如果有任何一个Promise失败,打印失败的结果
});
// 使用 Promise.race 方法组合多个Promise对象,返回一个新的Promise对象,该对象在任何一个Promise完成时完成(无论成功或失败)
let promise5 = new Promise((resolve,reject) => {
setTimeout(() => {
resolve(5);
},500); // 0.5秒后成功
});
let promise6 = new Promise((resolve,reject) => {
setTimeout(() => {
reject(6);
},1000); // 1秒后失败
});
let promise7 = Promise.race([promise5,promise6]); // 组合两个Promise对象
promise7.then((value) => {
console.log(value); // 5 (如果有任何一个Promise先成功,打印成功的结果)
}).catch((reason) => {
console.log(reason); // 如果有任何一个Promise先失败,打印失败的结果
});
// 使用 async/await 语法简化异步操作(需要在函数前加上 async 关键字,并且在等待的地方加上 await 关键字)
async function test() {
try {
let value = await promise; // 等待promise对象的结果(如果成功,赋值给value;如果失败,抛出异常)
console.log(`Success: ${value}`); // 如果成功,打印结果
} catch (error) {
console.log(`Fail: ${error}`); // 如果失败,打印错误
}
}
test(); // 调用异步函数
模块化
模块化是一种将代码分割为不同的文件或模块的方法,可以提高代码的可读性、可维护性和复用性。ES6提供了一种原生的模块化语法,可以让JavaScript支持导入和导出模块。导入模块使用 import 关键字,导出模块使用 export 关键字。
// 创建一个名为 math.js 的模块文件,并且导出两个函数:add 和 multiply
export function add(x,y) {
return x + y;
}
export function multiply(x,y) {
return x * y;
}
// 在另一个文件中,使用 import 关键字导入 math.js 模块,并且使用它们
import {add,multiply} from "./math.js"; // 导入指定的函数(需要使用花括号)
console.log(add(1,2)); // 3
console.log(multiply(2,3)); // 6
// 可以使用 as 关键字给导入或导出的函数起别名
import {add as plus,multiply as times} from "./math.js"; // 导入并重命名函数
console.log(plus(1,2)); // 3
console.log(times(2,3)); // 6
export {add as plus,multiply as times}; // 导出并重命名函数
// 可以使用 * 符号导入或导出所有的函数(需要起一个别名)
import * as math from "./math.js"; // 导入所有函数并起一个别名为math
console.log(math.add(1,2)); // 3
console.log(math.multiply(2,3)); // 6
export * from "./math.js"; // 导出所有函数
// 可以使用 default 关键字指定一个默认的导出(只能有一个,默认导出不需要花括号)
export default function subtract(x,y) {
return x - y;
}
import subtract from "./math.js"; // 导入默认导出(不需要花括号)
console.log(subtract(5,4)); // 1
迭代器和生成器
迭代器是一种遵循迭代协议的对象,可以按照一定的顺序访问一个集合中的元素。迭代器有一个 next 方法,每次调用返回一个包含 value 和 done 属性的对象,value 表示当前元素的值,done 表示是否还有更多元素。ES6提供了一种新的语法 for...of 循环,可以方便地遍历迭代器。
生成器是一种特殊的函数,可以返回一个迭代器对象,并且可以在函数体内使用 yield 关键字暂停和恢复执行。生成器使用 function* 关键字声明,并且可以接收参数。
// 创建一个迭代器对象:使用 Symbol.iterator 符号作为属性名,并且返回一个具有 next 方法的对象
let iterator = {
[Symbol.iterator]() {
let i = 0;
return {
next() {
if (i < 5) {
return {value: i++, done: false}; // 返回当前元素的值和状态
} else {
return {done: true}; // 返回结束状态
}
}
};
}
};
// 使用 for...of 循环遍历迭代器对象(不需要调用 next 方法)
for (let value of iterator) {
console.log(value); // 0 1 2 3 4
}
// 创建一个生成器函数:使用 function* 关键字,并且在函数体内使用 yield 关键字暂停和恢复执行
function* generator(n) {
for (let i = 0; i < n; i++) {
yield i; // 每次遇到 yield 关键字,返回当前值并暂停执行,直到下一次调用 next 方法
}
}
// 调用生成器函数返回一个迭代器对象(可以传入参数)
let iter = generator(5);
// 使用 for...of 循环遍历迭代器对象(不需要调用 next 方法)
for (let value of iter) {
console.log(value); // 0 1 2 3 4
}
Map和Set
Map和Set是两种新的数据结构,可以提供更高效和灵活的存储和操作方式。Map是一种类似于对象的集合,但是它可以使用任意类型的值作为键(而不仅仅是字符串)。Set是一种类似于数组的集合,但是它只存储唯一的值(不会出现重复)。
// 创建一个Map对象:使用 new 关键字,并且可以传入一个可迭代的数组作为初始值(每个元素是一个键值对数组)
let map = new Map([["name","Alice"],["age",18]]);
// 使用 set 方法添加或修改键值对(可以使用任意类型的值作为键)
map.set("gender","female");
map.set(true,"yes");
map.set([1,2],"array");
// 使用 get 方法根据键获取对应的值(如果不存在,返回 undefined)
console.log(map.get("name")); // Alice
console.log(map.get(true)); // yes
console.log(map.get([1,2])); // undefined (因为数组是引用类型,不相等)
// 使用 has 方法判断是否存在某个键
console.log(map.has("age")); // true
console.log(map.has("grade")); // false
// 使用 delete 方法删除某个键值对(返回一个布尔值表示是否删除成功)
console.log(map.delete("age")); // true
console.log(map.delete("age")); // false
// 使用 size 属性获取Map中的元素个数
console.log(map.size); // 4
// 使用 clear 方法清空Map中的所有元素
map.clear();
console.log(map.size); // 0
// 创建一个Set对象:使用 new 关键字,并且可以传入一个可迭代的数组作为初始值(重复的元素会被忽略)
let set = new Set([1,2,3,4,4]);
// 使用 add 方法添加元素(如果已经存在,不会重复添加)
set.add(5);
set.add(4);
// 使用 has 方法判断是否存在某个元素
console.log(set.has(3)); // true
console.log(set.has(6)); // false
// 使用 delete 方法删除某个元素(返回一个布尔值表示是否删除成功)
console.log(set.delete(2)); // true
console.log(set.delete(2)); // false
// 使用 size 属性获取Set中的元素个数
console.log(set.size); // 5
// 使用 clear 方法清空Set中的所有元素
set.clear();
console.log(set.size); // 0
// Map和Set都是可迭代的对象,可以使用 for...of 循环或扩展运算符遍历它们
let map = new Map([["name","Alice"],["age",18]]);
let set = new Set([1,2,3]);
// 使用 for...of 循环遍历Map或Set(Map的每个元素是一个键值对数组,Set的每个元素是一个值)
for (let [key,value] of map) {
console.log(`${key}: ${value}`); // name: Alice age: 18
}
for (let value of set) {
console.log(value); // 1 2 3
}
// 使用扩展运算符将Map或Set转换为数组(Map的每个元素是一个键值对数组,Set的每个元素是一个值)
let mapArr = [...map];
console.log(mapArr); // [["name","Alice"],["age",18]]
let setArr = [...set];
console.log(setArr); // [1,2,3]
浏览器的同源策略,它的重要性在哪里?
同源策略是指浏览器只允许同源的网页进行交互。同源指的是协议、域名和端口都相同。这个策略是为了保护用户的隐私和安全性。例如,一个网页只能访问同源的CSS、JavaScript等资源,不能访问其他网站的资源。这可以防止恶意网站利用用户的敏感信息进行攻击。
同源策略
所谓同源是指:域名、协议、端口相同。
同源策略又分为以下两种:
DOM 同源策略:禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。
XMLHttpRequest 同源策略:禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求。
为什么要有跨域限制
因为存在浏览器同源策略,所以才会有跨域问题。那么浏览器是出于何种原因会有跨域的限制呢。其实不难想到,跨域限制主要的目的就是为了用户的上网安全。
如果浏览器没有同源策略,会存在什么样的安全问题呢。下面从 DOM 同源策略和 XMLHttpRequest 同源策略来举例说明:
如果没有 DOM 同源策略,也就是说不同域的 iframe 之间可以相互访问,那么黑客可以这样进行攻击:
- 做一个假网站,里面用 iframe 嵌套一个银行网站 http://mybank.com。
- 把 iframe 宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。
- 这时如果用户输入账号密码,我们的主网站可以跨域访问到 http://mybank.com 的 dom 节点,就可以拿到用户的账户密码了。
如果没有 XMLHttpRequest 同源策略,那么黑客可以进行 CSRF(跨站请求伪造) 攻击:
- 用户登录了自己的银行页面 http://mybank.com,http://mybank.com 向用户的 cookie 中添加用户标识。
- 用户浏览了恶意页面 http://evil.com,执行了页面中的恶意 AJAX 请求代码。
- http://evil.com 向 http://mybank.com 发起 AJAX HTTP 请求,请求会默认把 http://mybank.com 对应 cookie 也同时发送过去。
- 银行页面从发送的 cookie 中提取用户标识,验证用户无误,response 中返回请求数据。此时数据就泄露了。
- 而且由于 Ajax 在后台执行,用户无法感知这一过程。
因此,有了浏览器同源策略,我们才能更安全的上网。
跨域的解决方法
从上面我们了解到了浏览器同源策略的作用,也正是有了跨域限制,才使我们能安全的上网。但是在实际中,有时候我们需要突破这样的限制,因此下面将介绍几种跨域的解决方法。
CORS(跨域资源共享)
CORS(Cross-origin resource sharing,跨域资源共享)是一个 W3C 标准,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通。CORS 背后的基本思想,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。
CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。
整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信。
浏览器将 CORS 请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
只要同时满足以下两大条件,就属于简单请求。
-
请求方法是以下三种方法之一:
HEAD
GET
POST -
HTTP 的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同时满足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不一样的。
HTTP协议和HTTPS协议,以及它们之间的区别。
HTTP协议是一种用于传输网页数据的协议,而HTTPS协议是在HTTP协议的基础上增加了SSL/TLS安全层来保证数据传输的安全性。HTTPS协议通过加密和解密技术来保护数据的机密性和完整性,可以防止数据被窃取或篡改。此外,HTTPS协议还可以验证网站的身份,防止钓鱼网站等安全问题。
HTTP
HTTP是超文本传输协议,全称“Hyper Text Transfer Protocol”,是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协议传输数据,互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准。设计HTTP的初衷是为了提供一种发布和接收HTML页面的方法。
HTTPS
HTTPS协议是由SSL/TLS+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全
HTTPS协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性。
HTTP 和 HTTPS 协议的区别
HTTP 和 HTTPS 协议的主要区别如下:
- HTTPS 协议需要 CA 证书,费用较高;而HTTP 协议不需要;
- HTTP 协议是超文本传输协议,信息是明文传输的,HTTPS 则是具有安全性的 SSL 加密传输协议;
- 使用不同的连接方式,端口也不同,HTTP 协议端口是80,HTTPS协议端口是 443;
- HTTP 协议连接很简单,是无状态的;HTTPS 协议是有SSL 和HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP 更加安全。
请解释一下什么是CSS盒模型,包括IE盒模型和标准盒模型。
CSS盒模型是CSS布局的基础,它包括内容(content)、内边距(padding)、边框(border)和外边距(margin)四个部分。IE盒模型和标准盒模型是两种不同的盒模型计算方式,其中IE盒模型将元素的实际宽度和高度分别等于内容宽度、内边距宽度、边框宽度和外边距宽度之和,而标准盒模型则将元素的实际宽度和高度分别等于内容宽度加上两侧内边距和边框的宽度,但不包括外边距。
盒模型
盒模型(Box Modle)可以用来对元素进行布局,由内到外包含实际内容、内边距、边框、和外边距四个部分
W3C盒子模型(标准盒模型)
box-sizing: content-box;
width和height指的是内容区域的宽度和高度
IE盒子模型(怪异盒模型)
box-sizing: border-box;
width和height指的是内容区域、边框、内边距总的宽度和高度
请解释一下什么是CSS Flexbox布局,并举例说明其使用场景。
CSS Flexbox布局是一种用于在容器中排列和对齐子元素的布局方式。它通过定义容器的flex属性来控制子元素的排列和对齐方式,可以方便地实现响应式布局和复杂的页面布局。
例如,可以使用Flexbox布局来创建一个具有多个项目的垂直或水平导航菜单、一个在多个列中排列内容的布局或一个灵活的卡片布局等。
Flex 布局是轴线布局,只能指定"项目"针对轴线的位置,可以看作是一维布局。
问题:请解释一下什么是CSS Grid布局,并举例说明其使用场景。
CSS Grid布局是一种二维布局系统,适用于构建复杂的网页布局。它可以将页面划分为行和列,然后在其中放置元素。Grid布局提供了强大的对齐、方向和顺序的控制功能,可以创建复杂的页面布局和响应式设计。例如,可以使用Grid布局来创建一个具有固定和灵活网格的网页布局、一个具有多个等宽列的布局或一个具有对齐元素的布局等。
Grid 布局则是将容器划分成"行"和"列",产生单元格,然后指定"项目所在"的单元格,可以看作是二维布局。Grid 布局远比 Flex 布局强大。
问题:请解释一下什么是前端路由,以及它的实现方式和应用场景?
前端路由是一种在前端实现页面导航的技术,通过JavaScript来控制页面的URL和内容的变化,而不需要刷新整个页面。
前端路由的实现方式有多种,包括基于Hash的路由、基于HTML5 History API的路由和基于服务端渲染的路由等。
它的应用场景包括单页面应用程序(SPA)、多页面应用程序(MPA)和移动应用程序等。
传统路由
当用户访问一个url时,对应的服务器会接收这个请求,然后解析url中的路径,从而执行对应的处理逻辑。这样就完成了一次路由分发。
前端路由
前端路由是不涉及服务器的,是前端利用hash或者HTML5的history API来实现的,一般用于不同内容的展示和切换。
前端路由要做的就是两点:
- 在页面不刷新的情况下实现url的变化
- 捕捉url的变化,根据url更改页面内容
hash
hash是url中#后面的部分,服务器会自动忽略这部分,但是前端可以通过location.hash来获取。
当hash发生变化时,会触发hashchange事件,我们可以为这个事件指定一个回调,这样就能在hash变化时进行不同内容的展示。
使用hash的好处是支持低版本浏览器和IE浏览器。
history API
是HTML5新推出的API,可以允许我们操作浏览器的会话历史记录。
- pushState与replaceState
history.replaceState(dataObj,title,url);
history.pushState(dataObj,title,url);
这两种方法都用于对浏览器的历史栈进行操作,pushState是向当前历史栈的栈顶增加数据,而replaceState则是替换当前栈顶的数据。
这两种方式都可以将url替换并且不刷新页面。
- popstate事件
当活动历史记录条目更改时,将触发popstate事件。
需要注意的是调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back())
这也就是说,我们在使用history API改变浏览器的url时,仍需要额外的步骤去触发 popstate 事件,例如调用 history.back() 或 history.forward() 等方法。
请解释一下什么是HTTP请求头中的"Cache-Control"字段及其作用?
"Cache-Control"是HTTP请求头中的一个字段,用于控制浏览器和其他缓存系统如何缓存和重新使用网页资源。
它有多个指令,如"public"、"private"、"no-cache"、"no-store"、"max-age"等,可以用于指定不同的缓存策略。例如,"public"指令表示资源可以被任何缓存系统缓存,"private"指令表示资源只能被单个用户的浏览器缓存,"no-cache"指令表示资源必须每次都被重新请求并重新验证。
问题:请解释一下什么是CSS中的伪类和伪元素,并举例说明其使用场景。
伪类和伪元素是CSS中的特殊选择器,它们可以用于选择和样式化HTML元素的特定状态或部分。
伪类主要用于选择处于特定状态的元素,如鼠标悬停、被点击、正在被输入等。
而伪元素则用于选择元素的特定部分,如元素的第一个字母、第一个行、插入内容等。
例如,可以使用伪类:hover来改变鼠标悬停时的样式,使用伪元素::before或::after来在元素内容前或后插入内容。
问题:请解释一下什么是前端中的模块化开发,以及它的实现方式和应用场景。
模块化开发是一种将代码拆分成独立模块,每个模块具有特定的功能和依赖关系,然后通过模块之间的交互来实现整个应用程序的开发方式。
在前端开发中,模块化开发可以提高代码的可维护性、可重用性和可扩展性。
常见的模块化实现方式包括CommonJS、AMD、ES6模块等。
模块化开发的应用场景包括大型项目、团队协作、代码共享等。
问题:请解释一下什么是前端中的路由守卫(Route Guards)及其作用和使用场景?
路由守卫是前端路由中的一种机制,用于控制路由的访问权限和执行某些逻辑。
它可以在路由切换前进行判断和拦截,以决定是否允许进入该路由或执行特定的逻辑。
路由守卫的作用包括控制页面访问权限、实现登录验证、执行异步操作等。
例如,在Vue Router中,可以使用路由守卫来控制路由的访问权限,确保只有登录的用户才能访问某些页面。
问题:请解释一下什么是前端中的动态导入(Dynamic Import)及其优势和使用场景?
动态导入是一种在运行时动态加载模块的方式。通过动态导入,可以将代码拆分成多个模块,然后在需要时动态加载它们。
动态导入的优势包括提高代码的加载性能、按需加载模块、实现懒加载等。
它的使用场景包括构建大型应用程序、优化性能、按需加载功能等。
问题:请解释一下什么是前端中的虚拟DOM(Virtual DOM)及其作用和使用场景?
虚拟DOM是一种在内存中模拟DOM树的技术。通过虚拟DOM,可以将页面的状态和结构以对象的形式存储在内存中,然后通过比较新旧虚拟DOM之间的差异来更新页面。
虚拟DOM的作用包括提高页面的渲染性能、减少不必要的DOM操作、实现高效的页面更新等。
它的使用场景包括构建大型单页面应用程序、优化渲染性能等。
问题:请解释一下什么是前端中的JavaScript中的事件冒泡(Event Bubbling)及其作用和使用场景?
事件冒泡是指在一个元素上触发的事件会向上级元素传递的过程。在事件冒泡过程中,事件会从最内层元素开始触发,然后逐级向上传递到最外层元素。
事件冒泡的作用包括实现事件的多级处理、解决事件处理函数的重复调用问题等。
它的使用场景包括构建复杂的交互效果、处理多级菜单等。
问题:Vue实例的生命周期钩子有哪些?
Vue实例的生命周期钩子包括beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy和destroyed。
问题:Vue中的生命周期钩子函数的作用和使用场景是什么?
生命周期钩子函数是Vue组件中用于控制组件生命周期的函数。
每个生命周期钩子函数都有特定的作用和使用场景,例如beforeCreate用于初始化数据、方法和事件监听器;
created用于在实例被创建后进行一些初始化的操作;
mounted用于将虚拟DOM与真实DOM进行对比并渲染视图等。
问题:Vue的响应式原理是什么? vue3
Vue的响应式原理是通过使用Object.defineProperty()来实现的。当数据发生变化时,Vue会自动更新DOM。
vue2 Object.defineProperty(obj, prop, descriptor)
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
语法:Object.defineProperty(obj, prop, descriptor)
var obj = {};
Object.defineProperty(obj,'hello',{
get:function(){
//我们在这里拦截到了数据
console.log("get方法被调用");
},
set:function(newValue){
//改变数据的值,拦截下来额
console.log("set方法被调用");
}
});
obj.hello//输出为“get方法被调用”,输出了值。
obj.hello = 'new Hello';//输出为set方法被调用,修改了新值
通过以上方法可以看出,获取对象属性值触发get、设置对象属性值触发set,因此我们可以想象到数据模型对象的属性设置和读取可以驱动view层的数据变化,view的数据变化传递给数据模型对象,在set里面可以做很多事情。
vue3 Proxy(target, handler)
let product = { price: 5, quantity: 2 }
let proxiedProduct = new Proxy(product, {
get(target, key, receiver) {
console.log('get')
// return target[key]
// 注意我们的get有一个额外的参数receiver,我们将它作为参数发送到Reflect.get中。这确保了当我们的对象从另一个对象继承了值/函数时,可以使用正确的this。这就是为什么我们总是在代理内部使用Reflect。
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log('set')
return Reflect.set(target, key, value, receiver)
}
})
问题:Vue中的组件是如何定义的?
Vue中的组件可以通过使用Vue.component()全局方法或局部注册方式进行定义。组件可以包含模板、数据、方法、生命周期钩子等。
问题:Vue中的props是什么?
props是组件的输入,是组件接收的数据。在父组件中,你可以将数据传递给子组件作为props。
问题:Vue中的slot是什么?
slot是用于在组件模板中插入自定义内容的一种机制。你可以通过使用slot标签来定义插槽内容。
问题:Vue中的插槽(slot)有什么作用?如何使用?
插槽的作用是在组件模板中插入自定义内容的一种机制。通过使用slot标签来定义插槽内容,可以将父组件的数据传递给子组件并渲染出相应的内容。
问题:Vue中的mixins是什么?
mixins是一种代码重用的方式,可以将一些可复用的代码片段定义为一个mixin对象,然后在多个组件中引用该mixin对象。
问题:Vue中的computed属性是什么?
computed属性是一种基于依赖关系计算属性的值的方法。你可以将计算属性定义为返回值的函数,该函数的返回值将自动与数据源同步。
问题:Vue中的watch和watchEffect有什么区别?
watch和watchEffect都是用于监听数据变化并执行相应操作的函数。watch需要手动调用,而watchEffect会自动监听数据变化并执行相应操作。
问题:Vue中的v-if和v-show有什么区别?
v-if和v-show都是用于控制元素是否显示的指令。
v-if是条件渲染指令,根据条件真假来决定是否渲染元素;而v-show是显示/隐藏元素指令,元素始终会被渲染,只是通过CSS来控制元素的显示/隐藏。
50.问题:Vue中的mixins和components有什么区别?
mixins和components都是Vue中用于代码重用的机制,但是它们之间存在一些重要的区别。
定义方式:mixins是通过定义一个包含组件选项的对象,并在组件中使用mixins选项将其混入到组件中;而components则是通过全局注册或局部注册方式定义组件,并在其他组件中使用。
作用范围:mixins的作用范围可以影响到使用它的所有组件,因为它是在全局范围内定义的;而components的作用范围仅限于定义它的父组件及其子组件。
冲突解决:当mixins和组件中存在相同的选项(例如数据、方法、生命周期钩子等)时,组件的选项会覆盖mixins的选项。因此,在使用mixins时需要注意避免命名冲突。
代码重用性:mixins可以包含一些通用的代码片段,可以在多个组件中重用;而components则是用于构建UI界面的独立单元,可以包含特定的模板、样式和逻辑。
总之,mixins和components都是Vue中用于代码重用的机制,但是它们之间存在一些重要的区别。需要根据具体的使用场景和需求来选择使用哪种机制。