likes
comments
collection
share

箭头函数和普通函数

作者站长头像
站长
· 阅读数 59

1、箭头函数

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

举个例子🌰:

const add = (x, y) => x + y;

const fn = (name) => ({ name });

箭头函数特性:

  • 语法简洁: 箭头函数的语法更为简洁,通常可以使用更少的代码来定义函数。
  • 没有自己的 this 箭头函数没有自己的 this 上下文,它会继承外层作用域的 this 值。
  • 不能作为构造函数: 箭头函数不能使用 new 关键字创建对象实例,也不能用作构造函数。
  • 不能使用 arguments 对象: 箭头函数没有自己的 arguments 对象,它会继承外层作用域的 arguments
  • 适合短小的函数: 由于简洁的语法,箭头函数特别适合定义简短的函数或用作回调函数。

2、普通函数

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

2.1、函数声明方式

function add(x, y) {
  return x + y;
}

2.2、函数表达式

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

2.3、使用Function构造函数

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; // 打印: "我是函数b中的name"
};
console.log(b());

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

2.4.2、执行效率

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

2.4.3、加载顺序

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

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

function f() {
  return 'JS 深入浅出';
}
var c = function () {
  return 'JS 深入浅出';
};

console.log(typeof c); // function

var d = new Function('return "JS 深入浅出"');
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.2、同名参数

在非严格模式下,函数中可以出现同名形参,且只能访问最后出现的该名称的形参。

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

而在严格模式下,出现同名形参会抛出语法错误。

function sum(x, x, x) {
  'use strict';
  return x;
}
console.log(sum(1, 2, 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 中定义函数的方式,它没有给函数起一个具体的名字标识符。匿名函数通常在需要时直接定义并使用,而不需要预先命名。这种函数在语法上与普通函数类似,只是省略了函数名。

匿名函数有多种用途,常见的包括:

2.7.1、函数表达式

将一个匿名函数赋值给变量,可以创建函数表达式,这使得函数能够像其他数据类型一样存储在变量中。

const add = function(x, y) {
  return x + y;
};

console.log(add(3, 5)); // 输出 8

2.7.2、作为参数传递

可以将匿名函数作为参数传递给其他函数,用于回调或执行特定操作。

setTimeout(function() {
  console.log('Delayed message');
}, 1000);

2.7.3、立即执行函数表达式(IIFE)

这是一种常见的模式,用于在定义后立即执行匿名函数,以创建一个私有作用域。

(function() {
  console.log('IIFE executed');
})();

2.7.4、函数属性

将匿名函数分配给对象属性,以创建对象的方法。

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.dir(fn);
console.dir(fn2);

箭头函数和普通函数

从打印结果来看,箭头函数与普通函数相比,缺少了 callerargumentsprototype

3.1、声明方式不同

  • 声明一个普通函数需要使用关键字function来完成,并且使用function既可以声明成一个具名函数也可以声明成一个匿名函数

  • 声明一个箭头函数则只需要使用箭头就可以,无需使用关键字function,比普通函数声明更简洁。

  • 箭头函数只能声明成匿名函数,但可以通过表达式的方式让箭头函数具名。

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

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

const b = function () {};
console.log(b.prototype); // {constructor: ƒ}

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,且 this 指向永远不会变,并且箭头函数没有原型 prototype,没法让它的实例的 __proto__ 指向箭头函数原型,所以箭头函数也就无法作为构造函数,否则用 new 调用时会报错。

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

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

function fn() {
  console.log('fn:', 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,而是引用的上层作用域中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指向' } this指向定义的时候外层第一个普通函数
  };
}

从上面例子中可以得出两点:

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

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

把上个例子中的 foo 函数修改一下,尝试直接修改箭头函数的 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 指向失败了,包括 aaplybind 都一样。它们(callaaplybind)会默认忽略第一个参数,但是可以正常传参。

所以箭头函数不能直接修改它的 this 指向。幸运的是,我们可以通过间接的形式来修改箭头函数的指向:去修改被继承的普通函数的this指向,然后箭头函数的this指向也会跟着改变,这在上一个例子中有演示。

bar.call(barObj); // 将bar普通函数的this指向barObj 然后内部的箭头函数也会指向barObj

3.5.3、函数外层有没有对象

箭头函数外层没有普通函数,严格模式和非严格模式下它的 this 都会指向 window(全局对象)

普通函数的默认绑定规则是:在非严格模式下,默认绑定的 this 指向全局对象,严格模式下 this 指向 undefined

3.6、箭头函数的 arguments

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

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

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

还是用这两个函数来比较,普通函数能够打印出 arguments,箭头函数使用 arguments 则会报错,因为箭头函数自身是没有 arguments 的,然后它会往上层作用域中去查找 arguments,由于全局作用域中并没有定义 arguments,所以会报错。

箭头函数处于普通函数的作用域中arguments 则是上层普通函数的 arguments

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

这里两个函数的 arguments 相同,都是 fn2 函数的 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,6]
};

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

需要⚠️注意的是:

  • rest参数只能作为函数的最后一个参数
let a = (a, ...rest, b) => {
  console.log(a, rest, b); // Uncaught SyntaxError: Rest parameter must be last formal parameter
};
a(1, 2, 3, 4);
  • 函数的length属性,不包括rest参数
const fn = function (...a) {};
console.log(fn.length); // 0

const fn2 = function (a, ...b) {};
console.log(fn2.length); // 1

rest参数与arguments的比较:

  • 箭头函数和普通函数都可以使用 rest 参数,而 arguments 只能普通函数使用。
  • 接受参数 restarguments 更加灵活。
  • rest 参数是一个真正的数组,而 arguments 是一个类数组对象,不能直接使用数组方法,需要使用扩展符([...arguments])或者 Array.from (Array.from(arguments)) 来将它转换成真正的数组。

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

箭头函数不支持重命名函数参数,而普通函数的函数参数支持重命名。

如下示例,普通函数的函数参数支持重命名,后面出现的会覆盖前面的,箭头函数会抛出错误:

function func1(a, a) {
  console.log(a, arguments); // 2 Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
}
func1(1, 2);

var func2 = (a, a) => {
  console.log(a); // Uncaught SyntaxError: Duplicate parameter name not allowed in this context
};
func2(1, 2);

3.9、箭头函数不可以使用 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 }; // 想返回一个对象,花括号被当成多条语句来解析,执行后返回undefined

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)。逻辑或运算符 || 是短路运算,如果左侧表达式的值为真(truthy),则整个表达式的结果为真,不会再计算右侧的表达式。因此,当左侧的表达式 false 时,右侧的箭头函数 () => true 并没有被执行,而是被当作整个表达式的值。

5、箭头函数不适用场景

5.1、定义字面量方法,this 的意外指向。

var name = 'hello';
var person = {
  name: 'hi',
  say: function () {
    console.log('say:', this.name); // say: hi
  },
  say2: () => {
    console.log('say2:', this.name); //say2: hello
  },
};

person.say();
person.say2();

上面代码中,person.say2() 方法是一个箭头函数,调用 person.say2() 时,使得 this 指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致 say2() 箭头函数定义时的作用域就是全局作用域。而 say() 定义的是一个普通函数,它内部的 this 就指向调用它的那个对象,所以使用普通函数符合预期。

5.2、当函数需要动态this时

下面是一个修改 dom 文本的操作,因为 this 指向错误,导致修改失败:

var button = document.querySelector('.btn');

button.addEventListener('click', () => {
  this.innerHTML = 'Clicked button'; // this又指向了全局
});

这里很显然会报错,因为按钮点击的回调是一个箭头函数,而箭头函数内部的 this 永远都是指向它的上层作用域中的 this,在这里就是 window,所以会报错。这里只需要将箭头函数改成普通函数就能正常调用了。

5.3、考虑代码的可读性,使用普通函数

  • 函数体复杂(具体表现就是箭头函数中使用多个三元运算符号,就是不换行,非要在一行内写完,非常恶心)。

  • 行数较多。

  • 函数内部有大量操作。

6、面试题

var name = 'global_name';
function Person(name) {
  this.name = name;
  this.foo1 = function () {
    console.log(this.name);
  };
  this.foo2 = () => console.log(this.name);
  this.foo3 = function () {
    return function () {
      console.log(this.name);
    };
  };
  this.foo4 = function () {
    return () => {
      console.log(this.name);
    };
  };
}

var person1 = new Person('person1_name');
var person2 = new Person('person2_name');

person1.foo1();
person1.foo1.call(person2);

person1.foo2();
person1.foo2.call(person2);

person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);

person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);






// 揭晓答案:
// person1.foo1(); => person1_name
// person1.foo1.call(person2); => person2_name

// person1.foo2(); => person1_name
// person1.foo2.call(person2); => person1_name

// person1.foo3()(); => global_name
// person1.foo3.call(person2)(); => global_name
// person1.foo3().call(person2); => person2_name

// person1.foo4()(); => person1_name
// person1.foo4.call(person2)(); => person2_name
// person1.foo4().call(person2); => person1_name

解析:

全局代码执行,person1 = new Person('person1_name'); person2 = new Person('person2_name') 执行完, person1 中的 this.nameperson1_nameperson2 中的 this.nameperson2_name,OK这一点清楚后,继续往下看:

  • 执行 person1.foo1()foo1 为普通函数,所以 this 应该指向 person1,打印出 person1_name
  • 执行 person1.foo1.call(person2)foo1 为普通函数,并且用 call 改变了 this 指向,所以它里面的 this 应该指向 person2,打印出 person2_name
  • 执行 person1.foo2()foo2 为箭头函数,它的 this 指向上层作用域,也就是 person1,所以打印出 person1_name
  • 执行 person1.foo2.call(person2),箭头函数的 this 指向无法使用 call 改变,所以它的 this 还是指向 person1,打印出 person1_name
  • 执行 person1.foo3()(),这里先执行 person1.foo3(),它返回了一个普通函数,接着再执行这个函数,此时就相当于在全局作用域中执行了一个普通函数,所以它的 this 指向 window,打印出 global_name
  • 执行 person1.foo3.call(person2)() 这个与上面类似,也是返回了一个普通函数再执行,其实前面的执行都不用关心,它也是相当于在全局作用域中执行了一个普通函数,所以它的 this 指向 window,打印出 global_name
  • 执行 person1.foo3().call(person2) 这里就是把 foo3 返回的普通函数this 绑定到 person2上,所以打印出 person2_name
  • 执行 person1.foo4()(),先执行 person1.foo4() 返回了一个箭头函数,再执行这个箭头函数,由于箭头函数的 this 始终指向它的上层作用域,所以打印出 person1_name
  • 执行 person1.foo4.call(person2)(),与上面类似只不过使用 call 把上层作用域的 this 改成了 person2,所以打印出 person2_name
  • 执行 person1.foo4().call(person2),这里是先执行了 person1.foo4(),返回了箭头函数,再试图通过 call 改变改变该箭头函数的 this 指向,上面我们说到箭头函数的this始终指向它的上层作用域,所以打印出 person1_name

7、总结

  • 箭头函数没有 prototype (原型),所以箭头函数本身没有 this

  • 箭头函数的 this 在定义的时候继承自外层第一个普通函数的 this

  • 如果箭头函数外层没有普通函数,严格模式和非严格模式下它的 this 都会指向 window (全局对象)。

  • 箭头函数本身的 this 指向不能改变,但可以修改它要继承的对象的 this

  • 箭头函数的 this 指向全局,使用 arguments 会报未声明的错误。

  • 箭头函数的 this 指向普通函数时,它的 arguments 继承于该普通函数。

  • 使用 new 调用箭头函数会报错,因为箭头函数没有 constructor

  • 箭头函数不支持 new.target

  • 箭头函数不支持重命名函数参数,普通函数的函数参数支持重命名。

  • 箭头函数相对于普通函数语法更简洁优雅。

转载自:https://juejin.cn/post/7265695216695672832
评论
请登录