一、前言
本文是由一道面试题展开的 关于Javascript函数与作用域的查遗补漏相关知识点:
- 函数
- 函数表达式 vs 函数声明
- 深入理解箭头函数
- 作用域
- 词法作用域
- 函数作用域
- 块作用域
var b = 10;
(function b() {
b = 20;
console.log(b)
})()
二、函数表达式 vs 函数声明
- 首先是语法
- 函数声明: 在主代码流中声明为单独的语句的函数。
//函数声明 function sum(a, b) { return a + b; } // 作为参数传递给函数的值,会被复制到函数的局部变量 // 函数可以访问外部变量。但它只能从内到外起作用。函数外部的代码看不到函数内的局部变量。 // 函数可以返回值。如果没有返回值,则其返回的结果是 undefined - 函数表达式: 在一个表达式中或另一个语法结构中创建的函数。下面这个函数是在赋值表达式 = 右侧创建的
// 函数表达式 let sum = function(a, b) { return a + b; };
- 细微差别:JavaScript 引擎会在什么时候创建
-
函数声明,在函数声明被定义之前,它就可以被调用。
sayHi("John"); // Hello, John function sayHi(name) { alert( `Hello, ${name}` ); } -
函数表达式,在代码执行到达时创建,并且仅从那一刻起可用。
sayHi("John"); // error! let sayHi = function(name) { // (*) no magic any more alert( `Hello, ${name}` ); };
简单方法区分
看function关键字出现在声明中的位置,如果是第一个词,那么就是一个函数声明,否则就是函数表达式。
// 这就是个函数表达式,千万注意📢
(function b() {
b = 20;
console.log(b)
})()
总结
- 函数是值。它们可以在代码的任何地方被分配,复制或声明。
- 如果函数在主代码流中被声明为单独的语句,则称为“函数声明”。
- 如果该函数是作为表达式的一部分创建的,则称其“函数表达式”。
- 在执行代码块之前,内部算法会先处理函数声明。所以函数声明在其被声明的代码块内的任何位置都是可见的。
- 函数表达式在执行流程到达时创建。 建议选择优先级:函数声明 > 函数表达式
三、深入理解箭头函数
基础:
对于一行代码的函数来说,箭头函数是相当方便的。它具体有两种:
- 不带花括号:
(...args) => expression— 右侧是一个表达式:函数计算表达式并返回其结果。 - 带花括号:
(...args) => { body }— 花括号允许我们在函数中编写多个语句,但是我们需要显式地return来返回一些内容。
引入箭头函数有两方面的所用: 更简短的函数并且不绑定this
深入:
- 箭头函数没有
this
如果访问this,则会从自己的作用域链的上一层继承this。
let group = {
title: "Our Group",
students: ["John", "Pete", "Alice"],
// 这里的 this.title 其实和外部方法 showList 的完全一样。那就是:group.title
showList() {
this.students.forEach(
student => alert(this.title + ': ' + student)
);
}
};
group.showList();
// 弹出:
// Our Group: John
// Our Group: Pete
// Our Group: Alice
正常的函数,则会报错:
报错是因为forEach运行它里面的这个函数,但是这个函数的this为默认值 this=undefined,因此就出现了尝试访问undefined.title的情况。
let group = {
title: "Our Group",
students: ["John", "Pete", "Alice"],
// 这个函数的 this 为默认值 this=undefined,因此就出现了尝试访问 undefined.title 的情况
showList() {
this.students.forEach(function(student) {
// Error: Cannot read property 'title' of undefined
alert(this.title + ': ' + student)
});
}
};
group.showList();
- 箭头函数没有
arguments当我们需要使用当前的 this 和 arguments 转发一个调用时,这对装饰器(decorators)来说非常有用。
例如,defer(f, ms) 获得了一个函数,并返回一个包装器,该包装器将调用延迟 ms 毫秒:
function defer(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms)
};
}
function sayHi(who) {
alert('Hello, ' + who);
}
let sayHiDeferred = defer(sayHi, 2000);
sayHiDeferred("John"); // 2 秒后显示:Hello, John
不用箭头函数的话,可以这么写:
function defer(f, ms) {
return function(...args) {
let ctx = this;
setTimeout(function() {
return f.apply(ctx, args);
}, ms);
};
}
在这里,我们必须创建额外的变量 args 和 ctx,以便 setTimeout 内部的函数可以获取它们。
- 不能对箭头函数进行
new操作 不具有 this 自然也就意味着另一个限制:箭头函数不能用作构造器(constructor)。不能用 new 调用它们。
JavaScript 函数有两个内部方法:[[Call]] 和 [[Construct]]。
当通过 new 调用函数时,执行 [[Construct]] 方法,创建一个实例对象,然后再执行函数体,将 this 绑定到实例上。
当直接调用的时候,执行 [[Call]] 方法,直接执行函数体。
箭头函数并没有 [[Construct]] 方法,不能被用作构造函数,如果通过 new 的方式调用,会报错。
- 箭头函数没有
super如果被访问,它会从外部函数获取
总结:
箭头函数是针对那些没有自己的“上下文”,但在当前上下文中起作用的短代码。
箭头函数没有自己的(this,arguments,super或new.target),但是可以用最近的外部函数的(this,arguments,super或new.target)
四、词法作用域
作用域: 作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。赋值操作符会导致LHS查询。=操作符或调用函数时传入参数的操作都会导致关联作用域的赋值操作。
作用域查找: 作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者向上进行,直到遇见第一个匹配的标识符为止。
词法作用域: 简单的说,词法作用域是由你在写代码时将变量和块作用域写在哪里决定的,因此当词法分析器处理代码时会保持作用域不变。
五、函数作用域
函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(在嵌套的作用域中也可以使用)
在任意片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。
var b = 10;
function foo() {
var b = 20;
console.log(b); // 20
}
foo();
console.log(b); // 10
foo被绑定在所在作用域中,可以通过foo()来调用它。
缺点是:
- 必须声明具名函数foo(),这样会污染所在作用域;
- 必须显示地通过函数名foo()调用这个函数才能运行其中的代码。
利用IIFE解决了上面的痛点:
- 不需要函数名,即函数名可以不污染所在作用域
- 能够自动运行
var b = 10;
(function foo() {
var b = 20;
console.log(b); // 20
})();
console.log(b); // 10
foo被绑定在函数表达式自身的函数中,而不是所在作用域中。
换句话说,(function foo() {..})()作为函数表达式意味着foo只能在..所代表的位置中被访问,外部作用域则不行。
六、前言面试题解析
两种情况:
var b = 10;
(function b() {
var b = 20 //差别行
console.log(b); //20
})()
var b 则在函数 b 内声明了一个局部变量,当执行 b = 20 时,顺着作用域链向上找,于是在函数内找到了局部变量 b(也就是 var b 的), 将其修改为 20。console.log(b)同理,顺着作用域链向上找,找到了局部变量 b,且其值为 20.
var b = 10;
(function b() {
b = 20 //差别行
console.log(b);
})()
//输出:
ƒ b() {
b = 20 //差别行
console.log(b)
}
执行 b = 20 时,顺着作用域链向上找,找到函数 b, 尝试给 b 赋值为 20,由于函数 b 是函数表达式,而函数表达式的函数名是常量,无法二次赋值(在正常模式下静默失效,在严格模式下报错),赋值失败,所以输出的还是该函数
//严格模式下的输出
var b = 10;
(function b() {
'use strict'
b = 20;
console.log(b)// "Uncaught TypeError: Assignment to constant variable."
})()
参见
-
《你不知道的JavaScript 上》