函数声明与箭头函数
函数是我们需要学习的一个重点, 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 关键字, 它只是更适合用于我们声明的函数表达式而不是函数声明;