js基础
1.ES6新特性
-
let 和 const: 引入块级作用域的变量声明关键字,
let用于声明变量,const用于声明常量。let variable = 10; const constant = 20; -
箭头函数: 提供了一种更短的语法来声明匿名函数。
// 传统函数 function add(x, y) { return x + y; } // 箭头函数 const add = (x, y) => x + y; -
模板字符串: 使用反引号(`)创建多行字符串和插入变量。
let name = "World"; let greeting = `Hello, ${name}!`; -
解构赋值: 允许从数组或对象中提取值并赋给变量。
// 数组解构赋值 let [a, b] = [1, 2]; // 对象解构赋值 let { x, y } = { x: 10, y: 20 }; -
默认参数值: 允许函数参数设置默认值。
function multiply(x, y = 2) { return x * y; } -
类: 引入了类的概念,提供了更简洁的面向对象编程语法。
class Person { constructor(name) { this.name = name; } sayHello() { console.log(`Hello, ${this.name}!`); } } let person = new Person("Alice"); person.sayHello(); -
Promise: 提供了更强大和灵活的异步编程模型。
function fetchData() { return new Promise((resolve, reject) => { // 异步操作 if (success) { resolve(data); } else { reject(error); } }); } -
模块化: 引入了模块的概念,允许将代码分割成小的、可维护的文件。
// 导出模块 export function add(x, y) { return x + y; } // 导入模块 import { add } from "./math"; -
Map 和 Set: 引入了新的数据结构,Map用于键值对的存储,Set用于存储唯一值。
let map = new Map(); map.set("key", "value"); let set = new Set(); set.add(1); set.add(2); -
Symbol: 引入了Symbol类型,用于创建唯一的标识符。
let mySymbol = Symbol("description");
2.var与let的区别
在JavaScript中,let 和 var 都是用于声明变量的关键字,但它们有一些关键的区别:
-
作用域:
var声明的变量具有函数作用域(function scope)或全局作用域(global scope)。let声明的变量具有块级作用域(block scope),它在{}内可见,而在外部是不可见的。
// 使用 var
function exampleVar() {
if (true) {
var x = 10;
}
console.log(x); // 输出 10,因为 var 具有函数作用域
}
// 使用 let
function exampleLet() {
if (true) {
let y = 20;
}
console.log(y); // 报错,因为 let 具有块级作用域,y 在这里不可见
}
-
变量提升:
- 使用
var声明的变量存在变量提升(hoisting)的现象,即变量可以在声明之前被访问,但值为undefined。 - 使用
let声明的变量也有变量提升,但在变量声明之前访问会导致ReferenceError,这有助于避免在变量初始化之前使用变量。
- 使用
// 使用 var,存在变量提升
console.log(a); // 输出 undefined
var a = 5;
// 使用 let,存在变量提升,但访问会导致 ReferenceError
console.log(b); // 报错 ReferenceError: b is not defined
let b = 10;
-
重复声明:
- 使用
var可以在相同作用域内重复声明同一变量。 - 使用
let不允许在相同作用域内重复声明同一变量。
- 使用
var x = 5;
var x = 10; // 合法,var 允许重复声明
let y = 15;
let y = 20; // 报错,不允许重复声明
-
全局对象属性:
- 使用
var声明的变量会成为全局对象的属性,而使用let声明的变量不会。
- 使用
var globalVar = 5;
console.log(window.globalVar); // 输出 5,globalVar 成为全局对象的属性
let localVar = 10;
console.log(window.localVar); // 输出 undefined,localVar 不是全局对象的属性
总体而言,推荐使用 let 和 const,因为它们提供更好的作用域规则和变量声明行为。使用 let 能够更好地避免一些常见的 JavaScript 陷阱。
3.防抖和节流
防抖(Debouncing)和节流(Throttling)都是用于处理频繁触发的函数,以提高性能和避免不必要的资源消耗。
-
防抖(Debouncing):
- 防抖的目标是确保一段时间内只执行一次函数。
- 当事件被触发后,等待一定的时间,如果在这段时间内再次触发了事件,则重新计时。只有在没有新的事件被触发时,才执行函数。
- 典型的应用场景是输入框输入验证、搜索框提示等需要等待用户停止输入的场合。
function debounce(func, delay) { let timer; return function() { timer && clearTimeout(timer); timer = setTimeout(() => { func.call(this); },delay); }; } -
节流(Throttling):
- 节流的目标是在一段时间内不论触发多少次事件,都只执行一次函数。控制高频事件执行次数。
- 它保证在指定的时间间隔内,事件处理函数只被执行一次,不管事件触发频率有多高。
- 典型的应用场景是页面滚动事件、窗口大小改变事件等高频触发的场合。
function throttle(func, delay) { let flag = true; if(flag){ setTimeout(()=>{ func.call(this); flag = true; },delay) flag = false; } }
使用场景:
- 防抖: 在需要等待用户停止某个操作后才执行的场合,如输入框输入验证、搜索框提示等。
- 节流: 在需要控制函数执行频率的场合,如页面滚动事件、窗口大小改变事件等。
选择防抖还是节流通常取决于具体的业务需求。例如,如果希望减少触发函数的频率并确保最终只执行一次,可以使用防抖;而如果希望在一定时间内均匀分布执行函数,可以使用节流。
4.call、apply、bind的区别
call、apply 和 bind 都是 JavaScript 中用于改变函数执行上下文(即 this 指向)的方法。它们之间的主要区别在于参数的传递方式和立即执行与否。
-
call:
call方法立即调用函数,将函数体内的this指向传递的第一个参数,并且可以接受多个参数,依次传递给函数。
function example(arg1, arg2) { console.log(this, arg1, arg2); } example.call({ name: 'John' }, 'arg1', 'arg2'); -
apply:
apply方法也立即调用函数,将函数体内的this指向传递的第一个参数,并且接受一个包含参数的数组。
function example(arg1, arg2) { console.log(this, arg1, arg2); } example.apply({ name: 'John' }, ['arg1', 'arg2']); -
bind:
bind方法返回一个新函数,不立即执行原函数,而是返回一个新函数。新函数的this被绑定到传递的第一个参数,但不执行原函数。可以随后调用新函数,并传递任意参数。
function example(arg1, arg2) { console.log(this, arg1, arg2); } const boundFunction = example.bind({ name: 'John' }, 'arg1'); boundFunction('arg2');
总结:
call和apply立即调用原函数,唯一区别在于参数的传递方式(单个参数 vs. 数组)。bind返回一个新函数,不会立即执行原函数,而是返回一个新函数,可以稍后调用,同时可以传递参数。- 所有这三种方法都用于改变函数执行上下文,即修改
this指向。
5.深拷贝
function deepClone(obj) {
// 如果是基本类型或 null,则直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 根据类型创建一个新的对象或数组
const newObj = Array.isArray(obj) ? [] : {};
// 递归克隆对象的每一个属性或数组的每一项
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key]);
}
}
return newObj;
}
6.实现继承的方法
-
原型链继承:
- 通过将子类的原型指向父类的实例来实现继承。
function Parent() { this.name = 'Parent'; } function Child() { this.childName = 'Child'; } Child.prototype = new Parent(); const childInstance = new Child();缺点:父类的引用类型属性会被所有子类实例共享,且无法向父类传递参数。
-
构造函数继承(借用构造函数):
- 在子类构造函数中使用
call或apply方法调用父类构造函数,实现属性的继承。
function Parent() { this.name = 'Parent'; } function Child() { Parent.call(this); this.childName = 'Child'; } const childInstance = new Child();缺点:无法继承父类原型上的方法,每个子类实例都有一份父类的副本。
- 在子类构造函数中使用
-
组合继承(原型链 + 构造函数):
- 结合原型链和构造函数的方式,克服各自的缺点。
function Parent() { this.name = 'Parent'; } function Child() { Parent.call(this); this.childName = 'Child'; } Child.prototype = new Parent(); const childInstance = new Child();解决了原型链继承的缺点,但仍然调用了两次父类构造函数。
-
原型式继承:
- 使用一个临时构造函数来实现继承。
function createObject(obj) { function F() {} F.prototype = obj; return new F(); } const parent = { name: 'Parent' }; const child = createObject(parent);缺点:引用类型属性仍然会被所有实例共享。
-
寄生式继承:
- 在原型式继承的基础上,增强对象,返回一个新对象。
function createObject(obj) { const clone = Object.create(obj); clone.sayHello = function() { console.log('Hello'); }; return clone; } const parent = { name: 'Parent' }; const child = createObject(parent); -
寄生组合式继承:
- 使用组合继承的方式,但优化了调用两次父类构造函数的问题。
function Parent() { this.name = 'Parent'; } function Child() { Parent.call(this); this.childName = 'Child'; } Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; const childInstance = new Child();解决了组合继承的缺点,只调用了一次父类构造函数。
-
ES6 类继承:
- 使用
class关键字来定义类和继承。
class Parent { constructor() { this.name = 'Parent'; } } class Child extends Parent { constructor() { super(); this.childName = 'Child'; } } const childInstance = new Child();ES6 提供了更简洁的语法来实现继承,支持
super关键字调用父类构造函数和方法。 - 使用
8.数组常用的方法
-
push():
- 在数组末尾添加一个或多个元素,并返回新的长度。会改变原始数组。
const fruits = ['apple', 'banana']; const newLength = fruits.push('orange'); // fruits 现在为 ['apple', 'banana', 'orange'] -
pop():
- 移除数组的最后一个元素,并返回该元素的值。会改变原始数组。
const fruits = ['apple', 'banana', 'orange']; const removedElement = fruits.pop(); // removedElement 为 'orange', fruits 现在为 ['apple', 'banana'] -
unshift():
- 在数组的开头添加一个或多个元素,并返回新的长度。会改变原始数组。
const fruits = ['banana', 'orange']; const newLength = fruits.unshift('apple'); // fruits 现在为 ['apple', 'banana', 'orange'] -
shift():
- 移除数组的第一个元素,并返回该元素的值。会改变原始数组。
const fruits = ['apple', 'banana', 'orange']; const removedElement = fruits.shift(); // removedElement 为 'apple', fruits 现在为 ['banana', 'orange'] -
concat():
- 连接两个或多个数组,返回一个新数组。
const fruits1 = ['apple', 'banana']; const fruits2 = ['orange', 'kiwi']; const combinedFruits = fruits1.concat(fruits2); // combinedFruits 为 ['apple', 'banana', 'orange', 'kiwi'] -
splice():
- 从数组中删除或替换元素,或者向数组中添加新元素。
const fruits = ['apple', 'banana', 'orange']; fruits.splice(1, 1, 'kiwi', 'grape'); // fruits 现在为 ['apple', 'kiwi', 'grape', 'orange'] -
slice():
- 返回数组的一部分,不修改原数组。
const fruits = ['apple', 'kiwi', 'grape', 'orange']; const slicedFruits = fruits.slice(1, 3); // slicedFruits 为 ['kiwi', 'grape'], fruits 仍然为 ['apple', 'kiwi', 'grape', 'orange'] -
indexOf() 和 lastIndexOf():
- 分别返回指定元素在数组中第一次和最后一次出现的索引,如果不存在则返回 -1。
const fruits = ['apple', 'kiwi', 'grape', 'orange']; const indexOfKiwi = fruits.indexOf('kiwi'); // 1 const lastIndexOfGrape = fruits.lastIndexOf('grape'); // 2 -
forEach():
- 遍历数组的每个元素,并对其执行提供的函数。
const fruits = ['apple', 'kiwi', 'grape', 'orange']; fruits.forEach((fruit, index) => { console.log(`${fruit} at index ${index}`); }); -
map():
- 创建一个新数组,其元素是对原数组元素调用提供的函数的结果。
const numbers = [1, 2, 3, 4]; const doubledNumbers = numbers.map(number => number * 2); // doubledNumbers 为 [2, 4, 6, 8]。
-
filter():
- 创建一个新数组,其中包含通过提供的函数实现的测试的所有元素。
const numbers = [1, 2, 3, 4, 5]; const evenNumbers = numbers.filter(number => number % 2 === 0); // evenNumbers 为 [2, 4] -
reduce():
- 对数组中的所有元素执行一个累加器函数,返回累加的结果。
const numbers = [1, 2, 3, 4, 5]; const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0); // sum 为 15 -
some() 和 every():
some()方法检测数组中是否至少有一个元素满足指定条件,every()方法检测数组中是否所有元素都满足指定条件。
const numbers = [1, 2, 3, 4, 5]; const hasEven = numbers.some(number => number % 2 === 0); // true const allEven = numbers.every(number => number % 2 === 0); // false -
find() 和 findIndex():
find()方法返回数组中满足提供的测试函数的第一个元素的值,findIndex()方法返回数组中满足提供的测试函数的第一个元素的索引。
const numbers = [1, 2, 3, 4, 5]; const evenNumber = numbers.find(number => number % 2 === 0); // 2 const evenIndex = numbers.findIndex(number => number % 2 === 0); // 1 -
flat() 和 flatMap():
flat()方法创建一个新数组,其元素是原数组的子数组的元素,flatMap()方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。
const nestedArray = [1, [2, [3, [4]]]]; const flatArray = nestedArray.flat(Infinity); // [1, 2, 3, 4] const numbers = [1, 2, 3, 4]; const doubledArray = numbers.flatMap(number => [number * 2, number * 3]); // doubledArray 为 [2, 3, 4, 6, 6, 9, 8, 12]
9.数组的forEach与map有何区别
forEach 和 map 都是 JavaScript 数组提供的迭代方法,但它们之间有一些关键的区别:
-
返回值:
forEach没有返回值(返回undefined),它只是用于迭代数组元素执行回调函数,不会创建新的数组。map返回一个新的数组,该数组的元素是原始数组的每个元素调用回调函数的结果。
const numbers = [1, 2, 3, 4]; // forEach const resultForEach = numbers.forEach((number) => { console.log(number); }); // resultForEach 为 undefined // map const resultMap = numbers.map((number) => { return number * 2; }); // resultMap 为 [2, 4, 6, 8] -
对原数组的影响:
forEach不会改变原数组,它仅用于迭代数组元素执行回调函数。map创建并返回一个新数组,原数组不会受到影响。
const numbers = [1, 2, 3, 4]; // forEach numbers.forEach((number) => { console.log(number); }); // numbers 仍然为 [1, 2, 3, 4] // map const doubledNumbers = numbers.map((number) => { return number * 2; }); // numbers 仍然为 [1, 2, 3, 4] -
使用场景:
forEach适用于在迭代过程中执行一些操作,但不需要生成新的数组。map适用于对原数组进行映射、转换,生成一个新的数组。
// 使用 forEach const numbers = [1, 2, 3, 4]; numbers.forEach((number, index, array) => { array[index] = number * 2; // 改变原数组 }); // 使用 map const doubledNumbers = numbers.map((number) => { return number * 2; // 创建新数组 });
总体而言,如果你只需要迭代数组元素执行一些操作而不需要生成新的数组,可以使用 forEach。如果需要对原数组进行映射、转换并生成新的数组,应该使用 map。