前端js面试题 (一)

1、请你阐述一下原型与原型链。

原型的定义:每一个JavaScript对象都有一个原型,它是一个指向另一个对象的引用,这个原型对象包含可以被继承的属性和方法。
每当你创建一个新的对象,如果这个对象的原型被关联到一个原型对象上,那么这个新对象上也可以使用原型对象的属性和方法。

原型链:如果对象A的原型是对象B,而对象B的原型又是对象C,那么对象A可以访问对象B和对象C上定义的属性和方法。对象ABC组成的这种多层对象的原型链条关系,称之为原型链。

// 创建一个对象 person
var person = {
    
    
  name: "John",
  age: 30,
};

// 创建一个对象 student,它的原型是 person
var student = Object.create(person);
student.grade = "A";

// 创建一个对象 teacher,它的原型是 person
var teacher = Object.create(person);
teacher.subject = "Math";

// 原型链的演示
console.log(student.name);     // 输出 "John",从原型链上继承了 name 属性
console.log(student.grade);    // 输出 "A",自身属性
console.log(teacher.name);     // 输出 "John",从原型链上继承了 name 属性
console.log(teacher.subject);  // 输出 "Math",自身属性

2、开发中的闭包问题。

闭包是指一个函数能够访问其外部函数作用域中的变量,即使外部函数已经执行完毕。
闭包问题:
1、闭包函数可以捕获并保持对其外部作用域中变量的引用,这意味着这些变量不会被垃圾回收机制回收,直到闭包不再被引用。

function outer() {
    
    
  var count = 0;
  function inner() {
    
    
    count++;
    console.log(count);
  }
  return inner;
}

var closureFunc = outer();
closureFunc(); // 输出 1
closureFunc(); // 输出 2

inner函数形成了一个闭包,可以访问outer函数作用域中的count变量,即使outer函数已经执行完毕。

2、由于闭包可以保持对外部作用域中变量的引用,如果不注意,可能会导致内存泄漏问题。当不再需要闭包时,需要手动解除对其的引用,以便垃圾回收机制可以回收相应的内存。

function outer() {
    
    
  var count = 0;
  function inner() {
    
    
    count++;
    console.log(count);
  }
  return inner;
}

var closureFunc = outer();
closureFunc(); // 输出 1
closureFunc(); // 输出 2
// 当不再使用时,清除对其的引用
closureFunc = null;

3、在循环中创建闭包时需要格外小心,因为闭包会捕获循环变量的值,可能导致意外行为所以尽量避免在循环的函数里面使用循环变量本身。

function createCounter() {
    
    
 let count = 0;
 return function() {
    
    
   count += 1;
   return count;
 };
}

const counter = createCounter();
for (let i = 0; i < 10; i++) {
    
    
 console.log(counter());
}

3、call、apply、bind的用途与区别

手写 call apply bind及它们区别 : https://dengxi.blog.csdn.net/article/details/120334431

4、手写一个promise

es6 promise知识点 王者段位前来挑战 https://dengxi.blog.csdn.net/article/details/122497871

5、箭头函数与普通函数

JavaScript中,箭头函数和普通函数有一些重要的区别,包括语法、作用域、this的绑定以及适用场景等方面的差异。
1、语法

// 普通函数
function regularFunction(a, b) {
    
    
  return a + b;
}

// 箭头函数
const arrowFunction = (a, b) => a + b;

2、作用域
普通函数:拥有自己的this关键字,并且在函数内部可以通过this引用函数被调用的上下文。
箭头函数:没有自己的this关键字,它的this是在定义时继承自外层的作用域。

3、this的绑定
普通函数:this的值在运行时根据调用方式(方法、函数、构造函数等)动态确定。
箭头函数:this的值在定义时确定,不会被改变。

    const obj = {
    
    
        name: "John",
        regularFunction: function () {
    
    
            console.log(this.name, 'regularFunction'); // 正确,this指向obj
        },
        arrowFunction: () => {
    
    
            console.log(this.name, 'arrowFunction'); // this指向window,因为箭头函数继承自外部作用域的this
        }
    };

    const regularFunc = obj.regularFunction;
    const arrowFunc = obj.arrowFunction;

    // 谁调用this就指向谁,相当于window在调用,
    regularFunc() // regularFunction
    // 箭头函数定义时外层作用域是window,箭头函数this指向window是固定的,所以this仍然指向window
    arrowFunc() // arrowFunction

    // 谁调用this就指向谁,obj在调用 this指向obj
    obj.regularFunction(); // John regularFunction
    // 箭头函数定义时外层作用域是window,箭头函数this指向window是固定的,所以this仍然指向window
    obj.arrowFunction(); // arrowFunction

4、arguments 对象
普通函数:普通函数内部可以访问到arguments对象,它包含了所有传递给函数的参数。
箭头函数:箭头函数没有自己的arguments对象,它会继承外部函数的arguments对象(如果有的话),或者根本不能访问arguments。

5、构造函数:
普通函数:普通函数可以用作构造函数,通过new关键字创建对象实例。
箭头函数:箭头函数不能用作构造函数,不能通过new关键字创建对象实例。

6、递归与尾递归。

递归(Recursion)
递归是一种函数调用自身的技术。在递归函数中,函数会不断地调用自身,直到满足某个终止条件,然后递归过程将逐层返回,将结果组合起来。
递归函数的调用栈会保存每次函数调用的上下文,直到达到终止条件,然后逐层返回。
递归函数的典型示例包括计算斐波那契数列、树的遍历等问题

function factorial(n) {
    
    
  if (n === 0) {
    
    
    return 1;
  } else {
    
    
    return n * factorial(n - 1);
  }
}

尾递归(Tail Recursion)
尾递归是一种特殊类型的递归,在尾递归函数中,递归调用是函数的最后一条语句。也就是说,在尾递归中,递归调用的返回值直接被返回给了当前函数的调用者,没有额外的计算操作。

尾递归的优势在于一些编程语言的优化器可以对其进行优化,将其转化为迭代循环,从而减少了函数调用栈的深度,提高了性能。

尾递归的经典示例是阶乘函数的尾递归实现。

function factorialTail(n, accumulator = 1) {
    
    
  if (n === 0) {
    
    
    return accumulator;
  } else {
    
    
    return factorialTail(n - 1, n * accumulator);
  }
}

7、await 返回值是什么。

await 关键字用于等待一个 Promise 解决(或拒绝),并暂停当前函数的执行,直到 Promise 的状态发生变化。

当 await 表达式成功完成时,它返回 Promise 解决时的值。如果 Promise 被拒绝(rejected),await 表达式将抛出一个错误。

只有在 async 函数中才能使用 await 关键字。如果你尝试在非异步函数中使用 await,会导致语法错误。

总之,await 返回 Promise 解决时的值,使得异步代码更容易理解和编写,因为它使异步操作看起来更像同步代码。

async function fetchData() {
    
    
  try {
    
    
    const response = await fetch('https://api.example.com/user');
    const data = await response.json();
    return data; // 返回成功时的数据
  } catch (error) {
    
    
    console.error('请求出错:', error);
    throw error; // 抛出错误
  }
}

在上面的代码中,await fetch(‘https://api.example.com/data’) 等待请求完成,并返回响应对象。然后,await response.json() 等待响应数据解析为 JSON,并返回解析后的数据。

8、promise.then,setInterval,Promise.resolve,执行顺序

正常情况下,Promise.resolve 是属于同步代码最先执行,promise.then 属于异步微任务,其次执行,setInterval才是真正异步方法,最后执行。

首先,promise.then中的回调函数被添加到Promise的回调队列中。
接下来,setInterval开始定期触发。
如果你在Promise上调用Promise.resolve(),并且Promise已经解决(resolved),则与该Promise相关的微任务(如.then回调)将在事件循环中的微任务阶段执行。

9、let const var 的区别。

1、作用域:
var:使用 var 声明的变量具有函数作用域(function scope)。这意味着它们只在包含它们的函数内部可见,而不在块级作用域内可见。
let 和 const:使用 let 和 const 声明的变量具有块级作用域(block scope)。这意味着它们在包含它们的代码块(例如,{})内可见,包括 if 语句、for 循环、while 循环等。

2、变量提升(Hoisting):
var 声明的变量会被提升到其作用域的顶部,即使在声明之前引用它们也不会报错。这意味着变量在声明之前会被初始化为 undefined。
let 和 const 也会被提升,但它们不会被初始化。如果在声明之前引用 let 或 const 变量,会导致暂时性死区(Temporal Dead Zone,TDZ)错误。

3、重复声明:
使用 var 可以多次声明同一个变量名,不会引发错误。
使用 let 或 const 在同一作用域内重复声明同一个变量名会引发错误。

4、可变性:
var 声明的变量可以随时重新赋值。
let 声明的变量也可以重新赋值,但它们可以在同一作用域内多次声明。
const 声明的变量是常量,一旦赋值后就不能再次赋值。但对于复杂数据类型(例如对象或数组),可以修改其内部内容,只要不改变变量本身的引用。

5、全局对象属性:
使用 var 声明的全局变量会成为全局对象(在浏览器中为 window)的属性。
使用 let 和 const 声明的全局变量不会成为全局对象的属性。

10、关于声明赋值的优先级。

1、变量声明提升(Hoisting):
在 JavaScript 中,var变量声明 会被提升到其所在作用域的顶部。这意味着变量的声明会在代码执行之前被解析,但赋值操作不会提升。

console.log(x); // 输出 undefined,不会报错
var x = 5;

2、赋值操作:
赋值操作按照代码的执行顺序进行。当代码执行到赋值语句时,右侧的表达式会被计算,然后结果将赋给左侧的变量。

a = 30;
var a;
var b = a + 5;
a = 10

在上述示例中,首先 a 被赋值为 30,然后 b 被赋值为 a + 5,因此 b 的值为 35,最后a重新被赋值10。

3、let 和 const 的暂时性死区(Temporal Dead Zone,TDZ):
使用 let 或 const 声明的变量存在暂时性死区,即在声明之前引用它们会引发错误

console.log(x); // 报错:x is not defined
let x = 5;

猜你喜欢

转载自blog.csdn.net/glorydx/article/details/133036641