前端面试小知识(JS)

变量声明和作用域

varlet, 和 const 是JavaScript中用于声明变量的关键字,它们在作用域和提升行为方面有所不同。

1.

var:

  • var 声明的变量具有函数作用域(如果在函数内声明)或全局作用域(如果在函数外声明)。
  • var 声明的变量会被提升到其所在作用域的顶部,但仅限于变量声明(不包括赋值)。这意味着在声明之前使用变量会返回 undefined
  • var 可以被重复声明,这可能导致意外的行为,特别是在大型代码库或团队协作中。

2.

let:

  • let 声明的变量具有块级作用域,即它们仅在声明它们的代码块(如 if 语句、for 循环等)内有效。
  • let 声明的变量不会被提升,尝试在声明之前访问变量会导致一个 ReferenceError
  • let 提供了块级作用域,这有助于避免变量提升带来的问题,并且使得代码更加清晰和易于管理。

3.

const:

  • const 声明的变量也具有块级作用域,并且必须在声明时初始化。
  • const 声明的变量同样不会被提升,且尝试修改已初始化的 const 变量会导致错误。
  • 使用 const 声明常量可以防止变量被重新赋值,有助于代码的可读性和维护性。

变量提升(Hoisting):

  • 变量提升是JavaScript中的一种机制,其中变量和函数声明在代码执行之前被移动到其所在作用域的顶部。
  • var 声明的变量和函数声明(使用 function 关键字)都会被提升。
  • let 和 const 声明的变量也会被提升,但它们不会被初始化,因此在声明之前访问它们会导致 ReferenceError

块级作用域与函数作用域:

  • 函数作用域:在函数内声明的变量或函数只在该函数内部可见。
  • 块级作用域:使用 let 和 const 声明的变量仅在包含它们的代码块(如 if 语句、for 循环等)内可见。这有助于限制变量的作用范围,减少全局变量的使用,从而降低代码中潜在的冲突和错误。

举例说明

例子 1: var 的变量提升和函数作用域

console.log(a); // 输出:undefined,因为 var a 被提升到了作用域顶部,但未初始化
var a = 5;

function exampleVar() {
  if (true) {
    var a = 10; // 同样被提升到函数作用域顶部
  }
  console.log(a); // 输出:10,因为 a 在 if 块内被重新赋值
}

exampleVar();
console.log(a); // 输出:5,函数作用域外的 a 未被影响

 

例子 2: let 的块级作用域和提升行为

console.log(b); // ReferenceError: b is not defined,因为 let b 没有被提升
let b = 5;

function exampleLet() {
  if (true) {
    let b = 10; // b 只在 if 块内有效
  }
  console.log(b); // 输出:5,块级作用域外的 b 未被影响
}

exampleLet();

例子 3: const 的块级作用域和提升行为

console.log(c); // ReferenceError: c is not defined,因为 const c 没有被提升
const c = 5;

function exampleConst() {
  if (true) {
    const c = 10; // c 只在 if 块内有效
  }
  console.log(c); // 输出:5,块级作用域外的 c 未被影响
}

exampleConst();

 闭包

闭包的定义和用途

定义:
闭包是JavaScript中的一个核心概念,它允许一个函数访问并操作函数外部的变量。更具体地说,当一个内部函数被其外部函数返回并在其他地方被调用时,它仍然可以访问外部函数的变量,即使外部函数已经执行完毕。这些变量不会被垃圾回收机制回收,因为内部函数仍然持有对它们的引用。

用途:

1.数据封装和隐私:闭包可以创建私有变量,因为它们只能通过闭包内部的函数访问。

2.模块化代码:闭包可以帮助组织代码,将相关的功能封装在一起。

3.维持状态:闭包可以用来保存和维持函数的状态,即使函数执行完毕后,状态仍然可以被后续的函数调用所访问。

4.回调函数和事件处理:闭包常用于处理异步操作和事件监听,因为它们可以保持必要的上下文信息。

闭包在实际开发中的应用场景

1.模块模式:
通过闭包实现模块化,可以创建具有私有变量和方法的模块。

const counter = (function() {
    let privateCounter = 0;
    function changeBy(val) {
        privateCounter += val;
    }
    return {
        increment: function() {
            changeBy(1);
        },
        decrement: function() {
            changeBy(-1);
        },
        value: function() {
            return privateCounter;
        }
    };
})();

console.log(counter.value()); // 输出:0
counter.increment();
counter.increment();
console.log(counter.value()); // 输出:2

2.回调函数:
在异步操作中,闭包常用于维持状态。

function wait(message) {
    setTimeout(function timer() {
        console.log(message);
    }, 3000);
}

wait('Hello, closure!'); // 3秒后输出:Hello, closure!

3.创建工厂函数:
使用闭包创建工厂函数,可以为每个实例创建独立的状态。

function makeMultiplier(x) {
    return function(y) {
        return x * y;
    };
}

const multiplyByTwo = makeMultiplier(2);
const multiplyByThree = makeMultiplier(3);

console.log(multiplyByTwo(4)); // 输出:8
console.log(multiplyByThree(4)); // 输出:12

 原型与继承

原型链的概念

在JavaScript中,每个对象都有一个内部链接指向另一个对象,即它的“原型”。这个原型对象自身也有一个原型,直到达到一个终点,这个终点通常是 Object.prototype。这个由原型对象组成的链条称为“原型链”。

原型链的主要用途是实现继承。当尝试访问一个对象的属性时,如果在该对象上找不到,JavaScript会继续在该对象的原型上查找,这个过程会一直持续到找到该属性或到达原型链的末端。

proto 与 prototype 的区别

  • prototype:

    • prototype 是函数的一个属性,当使用函数作为构造器创建对象时,通过 new 关键字创建的每个对象都会获得一个指向该函数 prototype 属性的内部链接。
    • 通过 prototype 属性,可以向构造函数创建的所有实例共享方法和属性。
  • proto:

    • __proto__ 是一个访问器属性(一个获取/设置函数),它包含对另一个对象的引用,这个对象被称为“原型”。
    • __proto__ 不是语言本身的一部分,而是浏览器实现的非标准特性,用于访问对象的原型。现代JavaScript推荐使用 Object.getPrototypeOf(obj) 和 Object.setPrototypeOf(obj, proto) 来获取和设置对象的原型。
    • 每个对象都有一个 __proto__ 属性,指向它的原型对象。

ES6 的 class 语法糖与传统的原型继承

  • 传统原型继承:

    • 在ES6之前,JavaScript使用函数和原型链来实现继承。
    • 通过设置构造函数的 prototype 属性,并使用 new 关键字来创建对象实例,可以实现继承。
  • ES6 的 class 语法糖:

    • ES6 引入了 class 关键字,它提供了一种更简洁和直观的方式来定义对象和实现继承。
    • class 实际上是基于原型继承的语法糖,它并没有引入新的继承模型,而是提供了一种更清晰和易于理解的语法。
// 传统原型继承方式
function Person(name) {
    this.name = name;
}

Person.prototype.greet = function() {
    console.log('Hello, my name is ' + this.name);
};

var person = new Person('Alice');
person.greet(); // 输出:Hello, my name is Alice

// ES6 class 语法糖
class PersonES6 {
    constructor(name) {
        this.name = name;
    }

    greet() {
        console.log('Hello, my name is ' + this.name);
    }
}

const personES6 = new PersonES6('Bob');
personES6.greet(); // 输出:Hello, my name is Bob

在上述例子中,Person 和 PersonES6 都创建了一个具有 greet 方法的对象。使用 class 关键字的版本更加简洁和易于理解,但其背后的工作机制与传统原型继承相同。

深浅拷贝的区别

浅拷贝:

  • 浅拷贝创建一个新对象,但只复制原始对象的引用值(如对象、数组)的第一层。
  • 如果原始对象中的属性是基本类型(如数字、字符串),则复制的是值本身。
  • 如果属性是引用类型(如对象、数组),则复制的是引用地址,新旧对象中的引用类型属性仍然指向同一个实例。

深拷贝:

  • 深拷贝创建一个新对象,并递归复制原始对象的所有层级,包括嵌套的对象和数组。
  • 新对象和原始对象完全独立,修改新对象不会影响原始对象。

如何实现深拷贝

浅拷贝的例子

let original = {
    name: 'Alice',
    age: 30,
    hobbies: ['reading', 'traveling']
};

// 浅拷贝
let shallowCopy = Object.assign({}, original);

shallowCopy.hobbies.push('coding');

console.log(original.hobbies); // 输出:['reading', 'traveling', 'coding']
console.log(shallowCopy.hobbies); // 输出:['reading', 'traveling', 'coding']

在这个例子中,shallowCopy 是 original 的浅拷贝。当我们向 shallowCopy.hobbies 添加 'coding' 时,original.hobbies 也发生了变化,因为 hobbies 数组是通过引用传递的。

深拷贝的例子

let original = {
    name: 'Alice',
    age: 30,
    hobbies: ['reading', 'traveling']
};

// 深拷贝
let deepCopy = JSON.parse(JSON.stringify(original));

deepCopy.hobbies.push('coding');

console.log(original.hobbies); // 输出:['reading', 'traveling']
console.log(deepCopy.hobbies); // 输出:['reading', 'traveling', 'coding']

在这个例子中,我们使用 JSON.parse(JSON.stringify(object)) 来创建 original 的深拷贝。现在,deepCopy 是完全独立的,修改 deepCopy.hobbies 不会影响到 original.hobbies

简单解释

  • 浅拷贝:创建了一个新对象,但对象内部的复杂类型(如数组、对象)仍然是引用原来的值。
  • 深拷贝:创建了一个新对象,并且递归复制了所有层级的属性,包括复杂类型的属性,使得新对象与原对象完全独立。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值