跳到主要内容

函数一等功臣

在 JavaScript 中,函数是一等公民(First-class Citizen),这意味着函数可以像其他数据类型一样被对待:可以赋值给变量、作为参数传递给其他函数、也可以作为函数的返回值。正是因为函数是一等公民,JavaScript 才能支持回调、高阶函数、闭包、柯里化等特性,这也是函数式编程得以实现的基础。

什么是尾调用优化和尾递归?​

尾调用指函数的最后一步操作是调用另一个函数。

// 有的引擎可以做到类似以下的优化,简化了函数的调用栈
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();

// 等同于
function f() {
return g(3);
}
f();

// 等同于
g(3);
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

"尾调用优化"对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6也是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署"尾调用优化"。这就是说,在 ES6 中,只要使用尾递归,就不会发生栈溢出,相对节省内存。

ES6箭头函数的特性​

ES6 增加了箭头函数,基本语法为 let func = value => value; 相当于

let func = function (value) {
return value;
};

箭头函数与普通函数的区别在于: 1、箭头函数没有 this,所以需要通过查找作用域链来确定 this 的值,这就意味着如 果箭头函数被非箭头函数包含,this 绑定的就是最近一层非箭头函数的 this, 2、箭头函数没有自己的 arguments 对象,但是可以访问外围函数的 arguments 对象 3、不能通过 new 关键字调用,同样也没有 new.target 值和原型

arguments​

arguments 是类数组对象,有 length 属性,不能调用数组方法 可用 Array.from()转换

箭头函数获取arguments​

可用…rest 参数获取

使用箭头函数(arrow functions)的优点是什么?​

  1. 语法简洁:箭头函数省略了 function 关键字,单条表达式可省略 return 和花括号,让代码更紧凑。
// 普通函数
const add = function (a, b) {
return a + b;
};
// 箭头函数
const add = (a, b) => a + b;
  1. 词法 this:箭头函数没有自己的 this,它会从外层作用域继承 this,避免了 that = this、bind(this) 这类写法,特别适合回调函数。
function Timer() {
this.count = 0;
setInterval(() => {
// 这里的 this 指向 Timer 实例,而不是 window
this.count++;
}, 1000);
}
  1. 没有自己的 arguments、super、new.target:它们都由外层作用域决定,简化了部分场景的参数获取(可用 ...rest 替代 arguments)。

  2. 不能作为构造函数:箭头函数没有 [[Construct]] 内部方法和 prototype 属性,使用 new 调用会抛出 TypeError,因此适合作为工具函数或回调,而非用来定义类型实例。

  3. 适合作为回调:由于语法简洁且 this 绑定明确,箭头函数非常契合 map、filter、setTimeout 等回调场景。

使用注意事项(不适合的场景):

  • 对象方法:对象内定义的方法如果依赖动态 this,使用箭头函数会导致 this 指向外层作用域而非对象本身。
  • 原型方法:同理,原型上的方法通常需要通过实例访问并绑定 this。
  • 需要 arguments 对象:应使用普通函数或 ...rest 参数。
  • 事件监听需要 this 指向触发元素:如 addEventListener 中需要 this 指向绑定元素时,不宜使用箭头函数。
// 反例:对象方法中使用箭头函数
const obj = {
name: 'Gavin',
sayHi: () => console.log(this.name), // this 指向外层,不指向 obj
};
obj.sayHi(); // undefined

说说async函数​

  1. 什么是 Async 函数?

Async 函数是一种异步函数,它使用 async 关键字在函数前进行修饰。它可以以一种更简单和直观的方式编写异步代码。Async 函数的返回值是一个 Promise 对象,可以使用 then 和 catch 处理。

  1. Async 函数与 Promise 之间有什么区别?

Async 函数是 Promise 的一种语法糖,它提供了一种更简单和直观的方式来编写异步代码。Promise 是一个对象,它代表一个异步操作的最终结果,可以使用 then 和 catch 处理。

  1. 如何在 Async 函数中使用 await?

在 Async 函数中,可以使用关键字 await 来等待一个 Promise 对象的解决。当遇到 await 表达式时,Async 函数会暂停执行,直到 Promise 对象被解决。

  1. Async 函数的返回值是什么类型?

Async 函数的返回值是一个 Promise 对象。它可以是解决的 Promise 对象,也可以是拒绝的 Promise 对象,具体取决于 Async 函数的执行结果。

  1. 在 Async 函数中可以使用哪些其他异步函数?

在 Async 函数中可以使用所有返回 Promise 对象的异步函数,例如 fetch、setTimeout、setInterval 等等。

  1. Async 函数在什么情况下会抛出异常?

当 Async 函数中的任何一条语句抛出异常时,该函数将立即停止执行并返回一个拒绝的 Promise 对象。这个 Promise 对象将包含抛出的错误对象。

  1. 在什么情况下我们应该使用 Async 函数?

通常情况下,当需要处理异步事件时,我们应该使用 Async 函数,因为 Async 函数提供了一种更直观和更易于理解的方式来编写异步代码。使用 Async 函数可以大大简化异步代码的编写和调试。

如果 new 一个箭头函数的会怎么样​

在 JavaScript 中,箭头函数是通过函数字面量定义的,它们没有构造函数。因此,无论如何使用 new 关键字来调用箭头函数,都会导致错误。

当你尝试使用 new 关键字来实例化一个箭头函数时,会抛出一个 TypeError 错误。这是因为箭头函数没有自己的 this 绑定,它们继承了父作用域的 this 值,并且无法被改变。使用 new 关键字来调用函数时,会创建一个新的对象,并将函数的 this 绑定到该对象上,但对于箭头函数来说,无法绑定新的 this,因此会导致错误。

以下是一个例子,展示了使用 new 关键字实例化箭头函数时会发生的错误:

const ArrowFunc = () => {
this.name = 'John'; // 错误:箭头函数没有自己的 this
};

const obj = new ArrowFunc(); // TypeError: ArrowFunc is not a constructor

总结来说,箭头函数不适用于被实例化,它们更适合作为匿名函数或用于简化函数语法。如果你需要创建可实例化的函数,应该使用常规的函数声明或函数表达式。

new 操作符的实现步骤​

在 JavaScript 中,使用 new 操作符来创建一个对象的实例时,会经历以下步骤:

  1. 创建一个空对象:首先,一个新的空对象会被创建。

  2. 设置原型链连接:新创建的对象会被设置其原型链([[Prototype]])指向构造函数的原型对象(Constructor.prototype)。

  3. 将构造函数的作用域赋给新对象:this 关键字会被绑定到新对象上,使构造函数内部的代码可以访问和操作新对象。

  4. 执行构造函数代码:构造函数会被执行,通过 this 关键字来对新对象进行属性赋值、方法定义等操作。

  5. 返回新对象:如果构造函数中没有显式返回其他对象,则 new 表达式会自动返回新创建的对象实例;否则,返回构造函数中显式返回的对象。

下面是一个简单的示例,演示了 new 操作符的实现步骤:

function Person(name, age) {
this.name = name;
this.age = age;
}

Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
};

const john = new Person('John', 25);
john.sayHello(); // 输出: Hello, my name is John and I'm 25 years old.

在上述示例中,new Person('John', 25) 执行过程如下:

  1. 创建一个空对象 john。

  2. 将 Person.prototype 设置为 john 的原型链。

  3. 将构造函数 Person 的作用域赋给 john,使 this 关键字绑定到 john。

  4. 执行构造函数内部的代码,将属性 name 和 age 赋值给 john。

  5. 返回 john 对象。

这样,我们通过 new 操作符创建了一个 Person 的实例,并可以使用该实例调用构造函数原型对象上的方法。

什么是尾调用,使用尾调用有什么好处?​

尾调用(Tail Call)是指函数在执行的最后一步调用另一个函数。具体来说,尾调用发生在函数的最后一个操作中,并且没有其他操作需要执行。尾调用可以出现在函数的返回语句中,或者是函数调用表达式的最后一部分。

尾调用优化(Tail Call Optimization)是一种优化技术,旨在在函数调用时减少内存的使用和栈的增长。当一个函数调用发生在另一个函数的尾调用位置时,可以将当前函数的调用帧替换为被调用函数的调用帧,而不会增加栈的深度。这样可以避免栈溢出错误,同时减少了内存的使用。

尾调用优化的好处包括:

  1. 减少内存占用:由于不需要保存每个函数调用的上下文,尾调用优化可以减少内存的使用。这对于递归函数特别有用,因为递归往往会导致栈溢出错误。

  2. 提高性能:尾调用优化可以减少函数调用的开销,使得程序的执行更加高效。通过消除不必要的调用帧的创建和销毁过程,可以节省时间和资源。

  3. 简化调试和代码阅读:尾调用优化可以使调用栈更加简洁和清晰,使调试和代码阅读更加容易。因为在尾调用优化的情况下,调用栈只会保留最后一次函数调用的信息,而不会堆积大量的调用帧。

需要注意的是,并非所有的 JavaScript 引擎都对尾调用进行优化。一些引擎对尾调用进行了优化,而另一些引擎可能没有进行优化。因此,在编写代码时,不能依赖于尾调用优化,而应该使用其他方法来避免栈溢出问题,例如使用迭代替代递归。

总结起来,尾调用是指函数在执行的最后一步调用另一个函数。尾调用优化可以减少内存的使用和栈的增长,提高性能,并简化调试和代码阅读。尽管不是所有的 JavaScript 引擎都支持尾调用优化,但在递归函数等情况下,尽量避免栈溢出错误仍然是一个好的编程实践。

如何确保你的构造函数只能被new调用,而不能被普通调用?​

function MyClass() {
// 确保通过 new 调用, 在ES6后建议只用 new.target来判断
if (!(this instanceof MyClass)) {
throw new Error('MyClass must be instantiated with new');
}

// 构造函数的其他代码...
}

// 正确的用法
const obj = new MyClass();

// 错误的用法
const obj2 = MyClass(); // 这里会抛出一个错误