JS基础

143 阅读11分钟

js基础

1.ES6新特性

  1. let 和 const: 引入块级作用域的变量声明关键字,let用于声明变量,const用于声明常量。

    let variable = 10;
    const constant = 20;
    
  2. 箭头函数: 提供了一种更短的语法来声明匿名函数。

    // 传统函数
    function add(x, y) {
      return x + y;
    }
    ​
    // 箭头函数
    const add = (x, y) => x + y;
    
  3. 模板字符串: 使用反引号(`)创建多行字符串和插入变量。

    let name = "World";
    let greeting = `Hello, ${name}!`;
    
  4. 解构赋值: 允许从数组或对象中提取值并赋给变量。

    // 数组解构赋值
    let [a, b] = [1, 2];
    ​
    // 对象解构赋值
    let { x, y } = { x: 10, y: 20 };
    
  5. 默认参数值: 允许函数参数设置默认值。

    function multiply(x, y = 2) {
      return x * y;
    }
    
  6. 类: 引入了类的概念,提供了更简洁的面向对象编程语法。

    class Person {
      constructor(name) {
        this.name = name;
      }
    ​
      sayHello() {
        console.log(`Hello, ${this.name}!`);
      }
    }
    ​
    let person = new Person("Alice");
    person.sayHello();
    
  7. Promise: 提供了更强大和灵活的异步编程模型。

    function fetchData() {
      return new Promise((resolve, reject) => {
        // 异步操作
        if (success) {
          resolve(data);
        } else {
          reject(error);
        }
      });
    }
    
  8. 模块化: 引入了模块的概念,允许将代码分割成小的、可维护的文件。

    // 导出模块
    export function add(x, y) {
      return x + y;
    }
    ​
    // 导入模块
    import { add } from "./math";
    
  9. Map 和 Set: 引入了新的数据结构,Map用于键值对的存储,Set用于存储唯一值。

    let map = new Map();
    map.set("key", "value");
    ​
    let set = new Set();
    set.add(1);
    set.add(2);
    
  10. Symbol: 引入了Symbol类型,用于创建唯一的标识符。

    let mySymbol = Symbol("description");
    

2.var与let的区别

在JavaScript中,letvar 都是用于声明变量的关键字,但它们有一些关键的区别:

  1. 作用域:

    • 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 在这里不可见
}
  1. 变量提升:

    • 使用 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;
  1. 重复声明:

    • 使用 var 可以在相同作用域内重复声明同一变量。
    • 使用 let 不允许在相同作用域内重复声明同一变量。
var x = 5;
var x = 10; // 合法,var 允许重复声明
​
let y = 15;
let y = 20; // 报错,不允许重复声明
  1. 全局对象属性:

    • 使用 var 声明的变量会成为全局对象的属性,而使用 let 声明的变量不会。
var globalVar = 5;
console.log(window.globalVar); // 输出 5,globalVar 成为全局对象的属性
​
let localVar = 10;
console.log(window.localVar); // 输出 undefined,localVar 不是全局对象的属性

总体而言,推荐使用 letconst,因为它们提供更好的作用域规则和变量声明行为。使用 let 能够更好地避免一些常见的 JavaScript 陷阱。

3.防抖和节流

防抖(Debouncing)和节流(Throttling)都是用于处理频繁触发的函数,以提高性能和避免不必要的资源消耗。

  1. 防抖(Debouncing):

    • 防抖的目标是确保一段时间内只执行一次函数。
    • 当事件被触发后,等待一定的时间,如果在这段时间内再次触发了事件,则重新计时。只有在没有新的事件被触发时,才执行函数。
    • 典型的应用场景是输入框输入验证、搜索框提示等需要等待用户停止输入的场合。
    function debounce(func, delay) {
      let timer;
      return function() {
        timer && clearTimeout(timer);
        timer = setTimeout(() => {
          func.call(this);
        },delay);
      };
    }
    
  2. 节流(Throttling):

    • 节流的目标是在一段时间内不论触发多少次事件,都只执行一次函数。控制高频事件执行次数。
    • 它保证在指定的时间间隔内,事件处理函数只被执行一次,不管事件触发频率有多高。
    • 典型的应用场景是页面滚动事件、窗口大小改变事件等高频触发的场合。
    function throttle(func, delay) {
        let flag = true;
        if(flag){
            setTimeout(()=>{
                func.call(this);
                flag = true;
            },delay)
            flag = false;
        }
    }
    

使用场景:

  • 防抖: 在需要等待用户停止某个操作后才执行的场合,如输入框输入验证、搜索框提示等。
  • 节流: 在需要控制函数执行频率的场合,如页面滚动事件、窗口大小改变事件等。

选择防抖还是节流通常取决于具体的业务需求。例如,如果希望减少触发函数的频率并确保最终只执行一次,可以使用防抖;而如果希望在一定时间内均匀分布执行函数,可以使用节流。

4.call、apply、bind的区别

callapplybind 都是 JavaScript 中用于改变函数执行上下文(即 this 指向)的方法。它们之间的主要区别在于参数的传递方式和立即执行与否。

  1. call:

    • call 方法立即调用函数,将函数体内的 this 指向传递的第一个参数,并且可以接受多个参数,依次传递给函数。
    function example(arg1, arg2) {
      console.log(this, arg1, arg2);
    }
    ​
    example.call({ name: 'John' }, 'arg1', 'arg2');
    
  2. apply:

    • apply 方法也立即调用函数,将函数体内的 this 指向传递的第一个参数,并且接受一个包含参数的数组。
    function example(arg1, arg2) {
      console.log(this, arg1, arg2);
    }
    ​
    example.apply({ name: 'John' }, ['arg1', 'arg2']);
    
  3. bind:

    • bind 方法返回一个新函数,不立即执行原函数,而是返回一个新函数。新函数的 this 被绑定到传递的第一个参数,但不执行原函数。可以随后调用新函数,并传递任意参数。
    function example(arg1, arg2) {
      console.log(this, arg1, arg2);
    }
    ​
    const boundFunction = example.bind({ name: 'John' }, 'arg1');
    boundFunction('arg2');
    

总结:

  • callapply 立即调用原函数,唯一区别在于参数的传递方式(单个参数 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.实现继承的方法

  1. 原型链继承:

    • 通过将子类的原型指向父类的实例来实现继承。
    function Parent() {
      this.name = 'Parent';
    }
    ​
    function Child() {
      this.childName = 'Child';
    }
    ​
    Child.prototype = new Parent();
    ​
    const childInstance = new Child();
    

    缺点:父类的引用类型属性会被所有子类实例共享,且无法向父类传递参数。

  2. 构造函数继承(借用构造函数):

    • 在子类构造函数中使用 callapply 方法调用父类构造函数,实现属性的继承。
    function Parent() {
      this.name = 'Parent';
    }
    ​
    function Child() {
      Parent.call(this);
      this.childName = 'Child';
    }
    ​
    const childInstance = new Child();
    

    缺点:无法继承父类原型上的方法,每个子类实例都有一份父类的副本。

  3. 组合继承(原型链 + 构造函数):

    • 结合原型链和构造函数的方式,克服各自的缺点。
    function Parent() {
      this.name = 'Parent';
    }
    ​
    function Child() {
      Parent.call(this);
      this.childName = 'Child';
    }
    ​
    Child.prototype = new Parent();
    ​
    const childInstance = new Child();
    

    解决了原型链继承的缺点,但仍然调用了两次父类构造函数。

  4. 原型式继承:

    • 使用一个临时构造函数来实现继承。
    function createObject(obj) {
      function F() {}
      F.prototype = obj;
      return new F();
    }
    ​
    const parent = {
      name: 'Parent'
    };
    ​
    const child = createObject(parent);
    

    缺点:引用类型属性仍然会被所有实例共享。

  5. 寄生式继承:

    • 在原型式继承的基础上,增强对象,返回一个新对象。
    function createObject(obj) {
      const clone = Object.create(obj);
      clone.sayHello = function() {
        console.log('Hello');
      };
      return clone;
    }
    ​
    const parent = {
      name: 'Parent'
    };
    ​
    const child = createObject(parent);
    
  6. 寄生组合式继承:

    • 使用组合继承的方式,但优化了调用两次父类构造函数的问题。
    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();
    

    解决了组合继承的缺点,只调用了一次父类构造函数。

  7. ES6 类继承:

    • 使用 class 关键字来定义类和继承。
    class Parent {
      constructor() {
        this.name = 'Parent';
      }
    }
    ​
    class Child extends Parent {
      constructor() {
        super();
        this.childName = 'Child';
      }
    }
    ​
    const childInstance = new Child();
    

    ES6 提供了更简洁的语法来实现继承,支持 super 关键字调用父类构造函数和方法。

8.数组常用的方法

  1. push():

    • 在数组末尾添加一个或多个元素,并返回新的长度。会改变原始数组。
    const fruits = ['apple', 'banana'];
    const newLength = fruits.push('orange');
    // fruits 现在为 ['apple', 'banana', 'orange']
    
  2. pop():

    • 移除数组的最后一个元素,并返回该元素的值。会改变原始数组。
    const fruits = ['apple', 'banana', 'orange'];
    const removedElement = fruits.pop();
    // removedElement 为 'orange', fruits 现在为 ['apple', 'banana']
    
  3. unshift():

    • 在数组的开头添加一个或多个元素,并返回新的长度。会改变原始数组。
    const fruits = ['banana', 'orange'];
    const newLength = fruits.unshift('apple');
    // fruits 现在为 ['apple', 'banana', 'orange']
    
  4. shift():

    • 移除数组的第一个元素,并返回该元素的值。会改变原始数组。
    const fruits = ['apple', 'banana', 'orange'];
    const removedElement = fruits.shift();
    // removedElement 为 'apple', fruits 现在为 ['banana', 'orange']
    
  5. concat():

    • 连接两个或多个数组,返回一个新数组。
    const fruits1 = ['apple', 'banana'];
    const fruits2 = ['orange', 'kiwi'];
    const combinedFruits = fruits1.concat(fruits2);
    // combinedFruits 为 ['apple', 'banana', 'orange', 'kiwi']
    
  6. splice():

    • 从数组中删除或替换元素,或者向数组中添加新元素。
    const fruits = ['apple', 'banana', 'orange'];
    fruits.splice(1, 1, 'kiwi', 'grape');
    // fruits 现在为 ['apple', 'kiwi', 'grape', 'orange']
    
  7. slice():

    • 返回数组的一部分,不修改原数组。
    const fruits = ['apple', 'kiwi', 'grape', 'orange'];
    const slicedFruits = fruits.slice(1, 3);
    // slicedFruits 为 ['kiwi', 'grape'], fruits 仍然为 ['apple', 'kiwi', 'grape', 'orange']
    
  8. indexOf() 和 lastIndexOf():

    • 分别返回指定元素在数组中第一次和最后一次出现的索引,如果不存在则返回 -1。
    const fruits = ['apple', 'kiwi', 'grape', 'orange'];
    const indexOfKiwi = fruits.indexOf('kiwi'); // 1
    const lastIndexOfGrape = fruits.lastIndexOf('grape'); // 2
    
  9. forEach():

    • 遍历数组的每个元素,并对其执行提供的函数。
    const fruits = ['apple', 'kiwi', 'grape', 'orange'];
    fruits.forEach((fruit, index) => {
      console.log(`${fruit} at index ${index}`);
    });
    
  10. map():

    • 创建一个新数组,其元素是对原数组元素调用提供的函数的结果。
    const numbers = [1, 2, 3, 4];
    const doubledNumbers = numbers.map(number => number * 2);
    // doubledNumbers 为 [2, 4, 6, 8]
  1. filter():

    • 创建一个新数组,其中包含通过提供的函数实现的测试的所有元素。
    const numbers = [1, 2, 3, 4, 5];
    const evenNumbers = numbers.filter(number => number % 2 === 0);
    // evenNumbers 为 [2, 4]
    
  2. reduce():

    • 对数组中的所有元素执行一个累加器函数,返回累加的结果。
    const numbers = [1, 2, 3, 4, 5];
    const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
    // sum 为 15
    
  3. 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
    
  4. 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
    
  5. 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有何区别

forEachmap 都是 JavaScript 数组提供的迭代方法,但它们之间有一些关键的区别:

  1. 返回值:

    • 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]
    
  2. 对原数组的影响:

    • 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]
    
  3. 使用场景:

    • 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