函数声明与箭头函数

  • Post author:
  • Post category:其他




函数声明与箭头函数

函数是我们需要学习的一个重点, 70%的面试官会问到与函数相关的问题, 95%的笔试题中会有与函数相关的问题。或者现在这个数据不是很准确了, 但仍然可以看出来这个方法的重要性。

  • 关于函数的 this

调用位置:调用位置就是函数在代码中被调用的位置(不是声明位置)

// 在这里声明
function fn() {
  console.log(this, 'fn');
}
// 在这里调用, 当前的调用位置是window, 所以, fn里的this指向就是window
fn();

某些时候, 我们可能无法一眼看出来函数的真正调用位置, 这里就需要分析

调用栈

了, 所谓的调用栈, 就是为了达到当前执行位置所调用的所有函数。也被称之为环境栈。

function fn() {
  // 当前的调用栈是 fn
  // 对应的调用位置是全局作用域window
  console.log(this, 'fn');
  fn1(); // fn1的调用位置
}
function fn1() {
  // 当前的调用栈是 fn -> fn1
  // 对应的调用位置是fn, fn的调用位置是window, 所以这里的this也指向window
  console.log(this, 'fn1');
  fn2(); // fn2的调用位置
}
function fn2() {
  // 当前的调用栈是 fn -> fn1 -> fn2
  // 对应的调用位置是fn - fn1, fn的调用位置是window, 所以这里的this也指向window
  console.log(this, 'fn2');
}
fn(); // fn的调用位置

默认绑定:默认绑定即独立的函数调用, 当其他规则无法应用时的默认规则, 如

function fn() {
  // 这里打印window.str
  console.log(this.str, 'fn');
}
var str = 'abc';
fn(); // 调用位置对应的是window, 所以fn的this指向window

隐式绑定:当函数有上下文对象时, 隐式绑定会将函数中的 this 指向到这个上下文对象。

function fn() {
  // 这里打印obj.str
  console.log(this.str, 'fn');
}
var str = 'abc';
var obj = {
  str: 'xyz',
  fn: fn,
};
obj.fn(); // 这里的上下文对应的调用位置是obj, 所以fn的this指向obj

对象的属性引用链只有上一层或者说最后一层会在调用位置起作用, 因为作用域链对于 this 的寻找只会到当前的活动对象或变量对象中, 不会到更上一层

function fn() {
  // 这里打印obj.str
  console.log(this.str, 'fn');
}
var str = 'abc';
const obj = {
  str: 'xyz',
  fn: fn, // fn的绑定环境在obj这里
};
const obj1 = {
  str: '123',
  obj: obj,
};
obj1.obj.fn(); // 调用位置对应的是obj, 所以fn的this仍然指向obj

显式绑定:能一眼看出来它的 this 指向的, 比如 call 或 apply

function fn() {
  console.log(this.str, 'fn');
}
var obj = {
  str: 'abc',
};
function fn1() {
  fn.call(obj); // 显式绑定this到obj, 所以fn中的this指向到obj
}
fn1();
fn1.call(window);

由于显式绑定是一种非常常用的方式, 所以 es5 中还有一种硬绑定的方式 bind

function fn() {
  console.log(this.str, 'fn');
}
var obj = {
  str: 'abc',
};
var fn1 = fn.bind(obj); // 通过bind的方式强制将fn方法与obj对象绑定到一起后返回一个新的函数
fn1();

通过 new 方式来绑定 this

function fn(str) {
  this.str = str;
}
var obj = new fn('abc');
obj.str;
// 这里的new操作可以这样来理解
{
  // 创建一个对象obj
  var obj = new Object();
  // 将新对象obj的内存地址指向到fn函数的原型对象
  obj.__proto__ = fn.prototype;
  // 利用函数的call方法, 将原本指向window的绑定对象指向了obj。这样一来,我们向函数中再传递实参时,对象的属性就被挂载到了obj上
  var result = fn.call(obj, 'abc');
  // 如果函数没有返回其它对象,那么就返回对象obj
  return result === 'object' ? result : obj;
}

this 绑定的优先级:

// 显式与隐式的比较
function fn() {
  console.log(this.str);
}
var obj = {
  str: 'abc',
  fn: fn,
};
var obj1 = {
  str: 'xyz',
  fn: fn,
};
obj.fn(); // abc, 隐式绑定, 通过上下文确定this环境指向obj
obj1.fn(); // xyz, 隐式绑定, 通过上下文确定this环境指向obj1
obj.fn.call(obj1); // 显式绑定, this指向obj1
obj1.fn.call(obj); // 显式绑定, this指向obj
// 可以看到,显式绑定的优先级要高于隐式绑定
// new操作符的比较
function fn(str) {
  this.str = str;
}
var obj = {
  fn: fn,
};
// 先通过new操作符绑定一个对象
var obj1 = new obj.fn('123');
var obj2 = {};
// 看看能否通过显式绑定来修改new操作符的指向
obj1.fn.call(obj2, 'xyz');
// 看看是否能通过隐式绑定来修改前面两个的this指向
obj.fn('abc');
console.log(obj.str, 'obj'); // 隐式绑定
console.log(obj1.str, 'obj1'); // 显式绑定
console.log(obj2.str, 'obj2'); // new 操作符绑定
// 可以看到,通过new操作符绑定的优先级要高于显式绑定
  • 总结:

    首先判断函数是否是在通过 new 操作符实例化对象中调用?如果是则指向这个实例化对象;

    如果不是实例化对象则判断是否显式绑定,如果是则指向显式绑定的对象;

    如果仍然不是,那么判断是否有上下文,如果有上下文则指向上下文对象;

    上面的条件都不符合,使用默认绑定,指向 window。

  • 箭头函数的 this

    箭头函数完全颠覆了函数声明的四种标准规则,而是在定义时函数时就保存了当前的作用域链,然后顺着当前的作用域链去寻找 this。也就是说,箭头函数的 this 在定义时就已经与对象绑定,而不是在调用时根据调用环境来决定 this 的指向。

    简单来说,箭头函数体内的 this 对象就是定义时所在的对象,而不是调用时所在的对象。

// 隐式绑定
const fn = () => {
  // 声明时发现当前作用域中没有this, 通过作用域链找到window
  console.log(this.str, 'fn');
};
var str = 'abc';
var obj = {
  str: 'xyz',
  fn: fn,
};
obj.fn(); // 这里的上下文是obj, 可是没用
// 我们刚刚在上面说到了, call某一个箭头函数时会忽略第一个参数, 所以我们无法显式地绑定它, 所以我们换个方式

function fn() {
  const fn1 = () => {
    console.log(this, 'fn1');
  };
  return {
    fn1,
    fn2: () => {
      console.log(this, 'fn2');
    },
  };
}
var str = 'abc';
var obj = {
  str: 'xyz',
};
var obj1 = {
  str: '123',
};
var fn3 = fn.call(obj); // 我们将fn的作用域显式地绑定到obj
var obj3 = fn3.fn1.call(obj1); // 无法再改变fn作用域里定义的箭头函数的this指向
var obj4 = fn3.fn2.call(obj1); // 无法再改变fn作用域里定义的箭头函数的this指向

总结:

箭头函数的 this 绑定看的是 this 所在的函数定义在哪个对象下,绑定到哪个对象则 this 就指向哪个对象

函数声明在一般情况下 this 的绑定是默认绑定,如果有 new 绑定则 new 绑定优先级最高,其次是显式绑定,然后再是隐式绑定。如果有对象嵌套的情况,则 this 绑定到最近的一层对象上。

  • 箭头函数的简捷性

如果只有一个参数, 可以省略圆括号, 如果没有参数或有多个参数则必须保留圆括号;

如果只有一行代码, 可以省略花括号;

如果返回的内容是一个表达式, 可以省略 return, 需要注意的是, 如果返回的是一个对象字面量表达式, 则需要以圆括号包围起来, 避免与函数体的{ … }起冲突;

  • 箭头函数比起传统的函数声明或函数表达式少了些什么?

没有绑定 this, 它的 this 是词法作用域, 所以在它的内部使用 this 关键字将指向上一层作用域;

let obj = {
  msg: 'hello',
  fn1: function (name) {
    console.log(this.msg, 'fn1'); // this指向obj
    function fn2() {
      console.log(this.msg, 'fn2'); // this指向window
      return `${this.msg}, ${name}`;
    }
    return fn2();
  },
};
obj.fn1('tom');
// undefined, tom

// 箭头函数的写法
let obj = {
  msg: 'hello',
  fn1: function (name) {
    console.log(this.msg, 'fn1'); // this指向obj
    fn2 = () => {
      console.log(this, 'fn2'); // this指向obj
      return `${this.msg}, ${name}`;
    };
    return fn2();
  },
};
obj.fn1('tom');
// 所以, 我们再也不需要使用that/self来保存当前this指向了

因为它无法对 this 进行绑定, 所以如果使用 call 或者 apply 方法调用时, 它们的第一个参数会被忽略;

let obj = {
  msg: 'hello',
  fn1: function (name) {
    console.log(this.msg, 'fn1'); // this指向obj
    fn2 = () => {
      console.log(this, 'fn2'); // this指向obj
      return `${this.msg}, ${name}`;
    };
    return fn2.call({ msg: 'hallo' });
  },
};
obj.fn1('tom');
// hello, tom
// this指针没有偏移到新对象

没有绑定 arguments 对象, 所以我们只能使用剩余参数(Rest, …);

没有自己的 super; 没有自己的 constructor; 也没有自己的 prototype, 所以它不能使用 new 关键字, 它只是更适合用于我们声明的函数表达式而不是函数声明;



版权声明:本文为marion_lau原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。