你真的了解箭头函数和普通函数吗?

王振宁 / 159 /

ChatGPT 可用网址,仅供交流学习使用,如对您有所帮助,请收藏并推荐给需要的朋友。
https://ckai.xyz

1. 箭头函数

ES6中允许使用箭头函数 => 来定义函数,箭头函数相当于匿名函数,并且简化了函数定义。

例:

const add = (x, y) => x + y
const fn = (name) => ({ name })

特性:

  • 语法简洁,代码量少。
  • 没有自己的 this,它会继承外层作用域的 this
  • 不能作为构造函数,不能用 new 关键字创建对象实例,也不能用作构造函数。
  • 不能使用 arguments 对象,它没有自己的 arguments 对象,会继承外层作用域的arguments
  • 适合短小的函数、或者作用回调函数

2. 普通函数

函数的定义方式通常有三种,函数声明方式函数表达式、使用 Function 构造函数;

2.1、 函数声明方式

function add(a, b) {
  return a + b
}

2.2、 函数表达式

const fn = function(name) {
  return { name }
}

2.3、 使用Function构造函数

const sum = new Function("num1", "num2", "return num1 + num2")

2.4、 三种方式的区别

主要从作用域效率加载顺序来区分

2.4.1、 作用域

函数声明、函数表达式声明、Function() 使用的都是局部变量

var name = "我是全局变量name"
// 声明式
function a() {
  var name = "我是函数a的name"
  return name;
}
console.log(a()) // 我是函数a的name


// 表达式
var b = function() {
  var name = "我是函数b的name";
  return name;
}
console.log(b()) // 我是函数b的name


// Function 构造函数
var c = new Function("const name = '我是函数c的name';return name;")
console.log(c()); // 我是函数c的name

2.4.2、 执行效率

Function() 构造函数效率要低于其他两种方式,尤其是在循环中,因为构造函数每执行一次都要重新编译,并生成新的函数对象

2.4.2、加载顺序

函数声明式在 JavaScript 编译时就会加载到作用域中,而其他两种方式则是在代码执行时加载,在定义之前调用它,会返回 undefined

console.log(typeof f) // function
console.log(typeof c) // undefined
console.log(typeof d) // undefined

function f() {
  return "f"
}

var c = function() {
  return "c"
}
console.log(typeof c) // function

var d = new Function("return 'd'");
console.log(typeof d) // function

2.5、函数的参数

2.5.1、arguments

arguments 对象的 length 属性显示实参的个数,函数的 length 属性显示形参的个数。

function sum(x, y) {
  console.log(arguments.length) // 3
  return x + 1;
}
sum(1, 2, 3);
console.log(sum.length); // 2

2.5.1、同名参数

非严格模式下,函数中可以出现同名形参,而且只能访问最后一个出现的形参

function sum(x, x, x) {
  return x;
}
console.log(sum(1, 2, 3)); // 3

严格模式下会抛出语法错误 SyntaxError: Duplicate parameter name not allowed in this context

2.6、函数的返回值

默认所有的函数都有返回值,没有 return 时,默认返回内容为 undefined

function sum1(x, y) {
  var total = x + y;
}
console.log(sum1()); // undefined

function sum2(x, y) {
  return x + y;
}
console.log(sum2(1, 2)); // 3

如果函数调用时,前面加了 new 前缀,且返回值不是一个对象,则返回 this,如果是一个对象,则返回该对象

function Book() {
  this.bookName = "JS 深入浅出"
}
var book = new Book();
console.log(book); // Book { bookName: "JS 深入浅出"}
console.log(book.constructor); // [Function: Book] 

function Book() {
  return { bookName: "JS 深入浅出"};
}
var book = new Book();
console.log(book); //  { bookName: "JS 深入浅出"}
console.log(book.constructor); // [Function: Book] 

2.7、匿名函数

匿名函数是一种在JavaScript 中定义的函数方式,他没有给函数起一个具体的名称,匿名函数通常在需要时直接定义使用,而不需要预先命名,这种函数在语法上与普通函数类似,只是省略了函数名。
用途:

  • 函数表达式,将匿名函数赋值给变量,可以创建函数表达式,这使得函数能像其他数据类型一样储存在变量中
const add = function(a, b) {
  return a + b;
}
console.log(add(3, 5)); // 8
  • 作为参数传递,用于回调或特定操作
setTimeout(function() {
  console.log("Delayed message")
}, 1000) 
  • 立即执行函数,可以创建一个私有作用域
(function() {
  console.log("IIFE executed")
})();
  • 函数属性,将匿名函数分配给对象属性,已创建对象的方法
const obj = {
 sayHello: function() {
   console.log("Hello")
 }
}
obj.sayHello(); // Hello

总之,匿名函数是一种在需要时临时创建函数的方式,适用于需要传递函数、作为参数、创建私有作用域、动态定义函数时;

普通函数的特性:

  • 拥有自己的 this: 普通函数在调用时会有自己的this 上下文,this 的值取决于函数被调用的方式
  • 可以作为构造函数:普通函数可以通过 new 关键字创建对象实例,用作构造函数。
  • 可以使用 arguments 对象:普通函数拥有自己的 arguments 对象,用于获取函数参数。
  • 语法相对复杂:需要 function 关键字来定义

3. 箭头函数与普通函数的区别

let fn = (name) => {
  console.log(name);
}

let fn2 = function(name) {
  console.log(name);
}

console.log(fn);
console.log(fn2);

image.png

看起来,箭头函数少了 caller arguments prototype

3.1、声明方式不同

  • 声明一个普通函数需要 function 来完成,并且使用 function 既可以声明成一个具名函数,也可以声明成一个匿名函数
  • 声明一个箭头函数则只要使用箭头就可以了
  • 箭头函数只能声明匿名函数,但可以通过表达式的方式让箭头函数具名

3.2、箭头函数没有 prototype(原型)

const a = () => {};
console.log(a.prototype); // undefined

const b = () => {};
console.log(b.prototype); // { constructor: f}

3.3、箭头函数不能当成一个构造函数

let fn = (value) => value;
const f = new fn("hi"); // Uncaught TypeError: fn is not a constructor

new 实现原理:

  • 创建一个新对象
  • 将该对象的原型链连接到构造函数的原型对象上,使其继承构造函数的属性和方法
  • 将构造函数中的 this 指向新创建的对象
  • 执行构造函数内部的代码,给新对象添加属性和方法
  • 如果构造函数没有返回其他对相关,则返回新创建的对象;如果构造函数返回了一个非基本类型的值,则返回这个对象,否则还是返回新创建的对象;
function myNew(fn, ...args) {
  // 创建一个新对象
  let target = {};

  // 将这个空对象的 __proto__指向构造函数的原型
  target.__proto__ = fn.prototype;

  // 将this 指向空对象
  let res = fn.apply(target, args)

  // 对构造函数返回值做判断,然后返回
  return res instanceof Object ? res : target;
}
因为箭头函数没有自己的this ,他的this 是外层执行环境的this ,且指向不会发生改变。并且箭头函数没有原型 prototype, 没法让他的实例的 proto 指向箭头函数的原型,所以构造函数无法作为构造函数。

3.4、箭头函数不支持 new.target

new 是从构造函数生成实例对象的命令,ES6new 命令引入了一个 new.target 属性,这个属性一般用在构造函数中,返回 new 调用的那个构造函数,如果构造函数不是通过 new 命令或者 Reflect.connstruct() 调用的,new.target 会返回 undefined,所以这个属性可以用来确定构造函数是怎么调用的;

function fn() {
  console.log("fnc", new.target);
}

fn(); // fn: undefined
new fn(); // fn: [Function: fn]

// 箭头函数的 this 指向全局对象时
let fn2 = () => {
  console.log("fn2", new.target);
}
fn2; // Uncaught SyntaxError: new.target expression is not allowed here

// 箭头函数this指向普通函数时
function func() {
  let test = () => {
    console.log(new.target); // 指向函数func: [Function: func]
  }
  test();
}

new func();
  • new.target 属性一般用在构造函数中,返回 new 调用的那个构造函数;
  • 箭头函数的 this 指向全局对象,在箭头函数中使用 new.target 会报错
  • 箭头函数的 this 指向普通函数,它的 new.target 就是指向改普通函数的引用

    3.5、this 指向规则

    对于普通函数来说,内部的 this 指向 函数运行时所在的对象,但是这一点对箭头函数不成立,他没有自己的 this 对象,内部的 this 就是定义时上层作用域的 this ,也就是说,箭头函数内部的 this 指向的是固定的,相比之下,普通函数 this 是可变的。

const name = "Hello";
const person = {
  name: "Hi”,
  say: function() {
    console.log("say", this.name);
  },
  say2: () => {
    console.log("say2", this.name)
  }
}
person.say(); // say: Hi
person.say2(); // say2: Hello

这里第一个 say 定义的是一个普通函数,并且它是作为对象 person 的方法来进行调用的,所以它的 this 指向是 person。 所以输出 say:Hi

而第二个 say2 定义的是一个箭头函数,我们知道箭头函数本身没有 this,它的 this 永远指向定义时所在的上层作用域,所以 say2this 应该指向的是 全局window, 所以他输出 say2:Hello

我们也可以通过 Babel 转箭头函数产生的 ES5 代码来验证箭头函数没有自己的 this ,而是引用的上层作用域

// ES6
function foo() {
  setTimeout(() => {
    console.log("id:", this.id)
  }, 100);
}

// ES5
function foo () {
  var _this = this;
  setTimeout(function() {
    console.log("id:", _this.id)
  }, 100);
}

转换后的 ES5 版本清楚的说明了,箭头函数里面没有自己的 this,而是引用上层作用域中的 this

3.5.1、箭头函数的 this 指向

箭头函数的 this 指向在定义的时候继承自外层第一个普通函数的 this。而普通函数他的 this 在运行时动态的绑定的,根据函数的调用方式和上下文而定。

let a;
let barObj = { msg: "bar的this指向" };
let fooObj = { msg: "foo的this指向" };

bar.call(barObj); // 将bar的this指向barObj
foo.call(fooObj); // 将foo的this指向fooObj

function foo() {
  a(); // 结果: { msg: "bar的this指向"}
};
 
function bar() {
  // 在 bar中定义的this继承了bar函数的this指向
  a = () => {
    console.log(this, "this指向定义的时候外层第一个普通函数"); // { msg: "bar的this指向" }
  }
}

从上面可以看出两点:

  1. 箭头函数的 this 指向定义时所在的外层第一个普通函数,跟使用位置有关系;
  2. 被继承的普通函数的 this 指向改变,箭头函数的 this 指向会跟着改变;

3.5.2、不能直接修改箭头函数的 this 指向

let a;
let barObj = { msg: "bar的this指向" };
let fooObj = { msg: "foo的this指向" };
let fnObj = { msg: "尝试直接修改箭头函数的this指向" };

bar.call(barObj);
foo.call(fooObj);

function foo() {
  a.call(fnObj); //  { msg: "bar的this指向" }
}

function bar() {
  a = () => {
    console.log(this);
  }
}

很明显,call 显示绑定 this 指向失败了,包括 applybind 都一样,他们 会默认忽略第一个参数,但是可以正常传参

所以箭头函数不能直接修改它的 this 指向,我们可以通过简介的形式来修改箭头函数的指向,去修改被继承的普通函数的 this 指向,然后箭头函数的 this 指向也会跟着改变。

3.5.3、函数外层没有对象

箭头函数外层没有普通函数,他的 this 会指向 window
普通函数的默认绑定规则:严格模式下,默认绑定 this 指向 window,严格模式下 this 指向 undefined

3.6、箭头函数的 arguments

箭头函数处于全局作用域中,则没有 arguments ,使用 arguments 汇报未声明错误,

let fn = () => {
  console.log(arguments);
}
fn(); // arguments is not defined

let fn2 = () => {
  console.log(arguments); // Arguments [callee: f, Symbol(Symbol.iterator): f]
}

普通函数可以打印出 arguments, 箭头函数使用 arguments 则会报错,因为箭头函数自身就没有 arguments 的,然后会向上层作用域去找 arguments, 没有就会报错。
箭头函数处于普通函数作用域中, arguments 则是上层普通函数的 arguments

let fn2 = function() {
  console.log("fn2:", arguments); // fn2: Arguments ['hi', callee: f, Symnol(Symbol.iterator): f]
  let fn = () => {
    console.log("fn:",arguments); // fn: Arguments ['hi', callee: f, Symnol(Symbol.iterator): f]
  }
};

两个 arguments 相同;
普通函数拥有自己的 arguments 对象,可以访问函数传入的参数,箭头函数没有自己的 arguments 对象,会共享外层作用域的 arguments
普通函数拥有自己独立作用域,而箭头函数没有独立作用域,会共享定义时的外层作用域。

3.7、rest 参数获取函数多余的参数

ES6 引入 rest 参数,用于获取函数不定数量参数数组,这个API是用来代替 arguments 的,形式为 ...变量名, rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

let fn = (a, ...args) => {
  console.log(a); // 1
  console.log(args); // [2, 3, 4, 5]
}

fn(1, 2, 3, 4, 5)

注:

  • rest 参数只能作为函数的最后一个参数;
  • 函数的 length 属性,不包括 rest 参数
  • rest 参数和 arguments 差异:
  • 箭头函数和普通函数都可以用 rest 参数,而 arguments 只能普通函数使用
  • 接受参数 restarguments 更加灵活;
  • rest 参数是一个真正的数组,而 arguments 是一个类数组对象,不能直接使用数组方法。

    3.8、箭头函数不能重复参数名

    普通函数参数名重复时,后面的参数会覆盖前面。

function func1 (a, a) {
  console.log(a, arguments); // Arguments(2) [1, 2, callee: f, Symbol(Symbol.iterator): f]
}
func1(1, 2);
var func2 = (a, a) => {
  console.log(a); // Uncaught SyntaxError: Duplicate parameter name not allowed in this context
}
func2(1, 2)

3.8、箭头函数不能使用 yield 命令

不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
这个可能是由于历史原因哈,TC39 在 2013 年和 2016 年分别讨论过两次,从 *()、*=>、=*>、=>* 中选出了 =>*,勉强进入了 stage 1。而且因为有了异步生成器(async generator) ,所以还得同时考虑异步箭头生成器(async arrow generator的东西,之前生成器 99.999% 的用途都是拿它来实现异步编程,并不是真的需要生成器本来的用途,自从有了 async/awaitgenerator生成器越来越没人用了。猜测可能是因为这个原因添加一个使用频率不高的语法,给规范带来较大的复杂度可能不值当。

4、箭头函数注意事项

4.1、一条语句返回对象时需要加括号

一条语句返回对象字面量,需要加括号,或者直接携程多条语句 return 形式 ,否则 func 中演示打的一样,花括号会被解析为多条语句的花括号,不能正确解析。

var func1 = () => { foo: 1}; // 想返回一个对象,花括号当成多条语句解析,执行后没有返回值

var func2 = () => ({ foo: 1}); // 用圆括号是正确的

var func2 = () => {
  return {
    foo: 1
  }
}

4.2、箭头函数在参数和箭头之间不能换行

var func = () 
 => 1; // 报错:Unexpected token =>

4.3、箭头函数的解析顺序相对 || 靠前

let a = false || function() {} // ok
let b = false || () => {}; // Malformed arrow function parameter list
let c = false || (() => {}); // ok

c 被赋值为一个逻辑表达式 false || (() => true) ,逻辑运算符 || 是短路运算,如果左侧表达式的值为真,则整个表达式的结果为真,不再计算右侧的表达式,因此,当左侧的表达式为 false 时,右侧的箭头函数 () => true 并没有被执行,而是被当做整个表达式的值。

虽然箭头函数中的箭头不是运算符,但箭头函数具有与常规函数不同的特殊运算符优先级的解析规则

5、箭头函数不适用场景


你真的了解箭头函数和普通函数吗?
作者
王振宁
许可协议
CC BY 4.0
发布于
2023-09-20
修改于
2025-02-02
Bonnie image
尚未登录