JavaScript学习笔记: 函数
概念
在js中,函数与其他类型一样,是一个支持所有操作的值,是一个对象,是编程语言里的“一等公民”
函数是一个代码块,每被调用一次,其代码就会执行一次。
函数有一个被{}
包裹的函数体,具体的逻辑代码就写在里面。
使用return关键字返回函数的计算结果,如果没有返回值,那函数调用表达式的值就是undefined。
在函数被调用时其函数体会生成一个独立的上下文环境,叫做函数作用域。在函数作用域内可以访问该函数所有的上层作用域,一直到顶级作用域,但是上层的作用域不能访问函数作用域。
定义函数
在JS中使用function关键字或箭头函数的方式来定义定义一个函数。
function关键字
-
function语句
使用function关键字后面跟表示函数名称的标识符,再跟着圆括号包裹以逗号分隔的任意数量的形参,形参允许指定默认值,后面再跟着花括号包裹的函数体。
以function语句的方式创建函数,会在函数被定义时所在的上下文生成一个以函数名命名的变量,变量的值就是函数本身。如果变量是顶级作用域,还会在全局对象上新建一个同名属性。
另外,函数声明会像var定义变量那样,将定义的函数“提升”到其上下文的顶部。但与var定义变量还有些许不同,var提升变量不会赋值,但function关键字会赋值。
console.log(fn); // ƒ fn (param1, param2 = 12) {} function fn (param1, param2 = 12) {}
-
function表达式
因为函数是一个值,所以我们可以将其赋值给变量或常量,这种定义方式叫做“函数表达式”。
定义函数表达式是,依然使用functioin关键字,但是可以省略函数名。
如果依然指定了函数名,js会在函数被调用时,在其函数体内生成同名变量并指向函数对象本身,该变量的作用域也仅限该函数体内部,在执行结束后销毁该变量。
使用函数的最佳实践是使用函数表达式将函数对象赋值给一个常量,常量是不可修改的。这样可以避免意外地修改了变量,而导致函数不能使用的问题。
const fn = function() {}; const func = function f () {console.log(f, f===func)}; // ƒ f () {console.log(f, f===func)} true func();
箭头函数
ES6新增的箭头函数语法,它使用箭头分隔参数与函数体,使我们定义函数就像在写数学函数那样。
箭头函数是一个表达式,就像function表达式那样。
如果函数的参数只有一个,可以省略包裹参数的括号;而当函数体只有一个返回语句,就可以省略包裹函数体的括号,以及返回语句的return关键字。
箭头函数是很适合作为参数传给其他函数的,这会让代码看起来比传入一个指向函数对象的变量更清晰,比传入一个函数表达式更简洁。
const fn = (p1, p2) => {};
const func = p => p*p;
[1,2,3].forEach((v, k)=> {console.log(`${k}: ${v}`)});
[1,2,3].map(v=>v*v);
嵌套函数与闭包
可以在函数体内再定义函数,被嵌套在其他函数内的函数被称为嵌套函数或子函数。
嵌套函数可以访问包含它的函数或更外层函数的变量
调用函数
作为函数调用
调用函数只需要在函数值后面跟包裹参数的圆括号即可。
function fn () {}
fn();
作为对象方法调用
函数还可以作为对象的属性,称之为对象方法。
let obj = {fn: ()=>{}}
obj.fn();
obj?.fun(); // 条件式访问属性并在属性为函数时调用,称为条件式调用
当对象的成员方法返回值也是一个对象,那还可以在表达式后面继续调用其他方法,这种调用方式称为链式调用,这种代码结构称为方法调用链。
Promise.resolve(1).then(()=>{}).then(()=>{});
作为构造函数调用
使用new关键字调用一个函数,就是构造函数调用。这个调用创建一个了一个类型为该构造函数的对象,该对象继承了被调用函数的原型。
所谓
function A () {}
a.prototype.name = 'Tom';
a.prototype.age = 3;
new A().name; // 'Tom'
(new A).age; // 3
Object.getPrototypeOf(new A) === A.prototype; //true
使用构造函数对象的call()与apply()方法调用
Function对象是函数对象的构造函数,它有call()与apply()两个方法。我们创建的函数对象继承了它们,可以用于调用他们自身。
call()与apply()的第一个参数用于为函数指定this值,可以是任何类型的值,但会忽略null或undefined类型。
如果传入的是一个对象指针,那就可以理解为作为那个对象的方法调用,但是并不会给传入的对象添加方法或覆盖其方法。
对于箭头函数,其this值保持为闭合词法上下文,也就是就是被定义时的上下文环境,不可被修改,所以对箭头函数使用这两个方法,会忽略第一个参数。
二者不同的是后续的参数,call()方法接收更多参数,用于传给要调用函数,而apply()方法的第二个参数接收一个数组或类数组对象,其包含了调用函数所使用的参数。
function fn(a,b) {console.log(this,a,b)}
fn.call(null, 'a', 'b'))
fn.call({a:1},'a', 'b');
fn.apply({a:1}, ['a', 'b']);
fn.apply({a:1}, {0: 'a', 1: 'b', length: 2});
使用bind()方法
bind()方法会返回一个新的函数,返回的函数是一个代理,调用该代理函数时,它为目标函指定this值,并将参数传递给目标函数。
使用bind()方法,就好比定义了一个调用call()的函数。
function fn(a,b) {console.log(this,a,b)}
let proxyFn = fn.bind({property: 1});
proxyFn('a', 'b')
// 模拟
function proxyCall(func, thisArg, ...args) {
func?.call(thisArg, ...args);
}
proxyCall(fn, {property: 1}, 'a', 'b')
函数是值
函数是“一等公民”,是一个对象值。
对象是可以被添加属性的,给函数对象添加一个属性,可以在每一次调用函数时都能访问到。
给函数对象添加属性,而不是在相同的上下文定义变量,可以避免污染命名空间,是很实用的技术。
function incNumber() {
return ++incNumber.cache;
}
incNumber.cache = 0;
函数作为命名空间
想要对代码进行拆分,希望拆分后定义的变量互不影响,可以使用函数。每个函数体都是一个独立的命名空间,代码互不干扰。
function mainNamespace() {
let foo=0;
const bar=1;
function fn(){}
}
function anotherNamespace() {
let foo=0;
const bar=1;
function fn(){}
}
mainNamespace();
anotherNamespace();
(()=>{
let foo=0;
console.log(foo);
})();
立即执行函数
有多种写法。但是要注意分组操作符的语法规则。
圆括号作为函数调用语法的一部分的时候,可以没有内容,但是作为分组操作符时,需要包含表达式。
/**
错误的写法:
funtion(arg) {console.log("IIFE");}();
上面的代码会报语法错误。
因为圆括号左侧是一条声明语句,并没有运算得到一个值,整段代码不是作为函数调用,右侧的括号解析为分组操作符。
解析器对待它就好比如下代码:
funtion(arg) {console.log("IIFE");};
();
把声明语句与分组操作符作为两条语句来解析。
虽然右侧圆括号内包裹了值,就不会报错,但是这改变不了JS的解析规则,依然不是函数调用。
将声明语句包含在外分组操作符内就会计算该声明,得到一个值,此时整个代码就是一个函数调用的语句,右侧的圆括号就可以省略值了。
但在整段代码外面加一个括号,这个括号是作为分组操作符被解析。整段代码变成了一个表达式,表达式内的函数定义语句会被计算,函数自然会被调用了。
同理,在整段代码前面加一个运算符,定义语句也会被计算,这样函数也会被调用。
只要这个运算符的语法要求是右侧有值就行(一元元算符)。
JS的箭头函数是一个表达式,只能作为一个语句的一部分。按照js的语法,表达式后面应该跟着像分号或换行符这样的分隔符,又或者作为分组操作符的一部分。所有下面的代码是不符合语法的:
(()=>{}());
*/
// 正确的写法
(funtion() {console.log("IIFE");})();
(funtion() {console.log("IIFE");}());
! funtion(arg) {console.log("IIFE");}();
(()=>{})();
闭包
函数使用词法作用域,意思是函数执行时使用的变量作用域是函定义函数时的上下文环境,而不是调用函数时的上下文环境。为了实现词法作用域,函数对象内部除了函数代码,还要持有定义函数时的作用域引用。这种将函数与其作用域组合起来解析函数变量的机制被称为闭包。
具体实现是,函数对象持有被其定义时的语法作用域内的所有变量的引用。要访问某个自身未定义的变量,就会查找其持有的那些变量中有没有同名的,有就返回它;对于函数自身有定义,定义函数的作用域也有定义的变量,就使用自身的。这跟JS对象的属性继承机制——原型链继承
简直一模一样。
function fn() {
let foo = 1, bar = 2;
function sub() {
let bar = 200;
console.log(foo,bar)
}
return sub;
}
let subFn = fn()
let foo = 100;
subFn(); // 1 200
函数调用时的this
-
在非严格模式下
对于functio关键字定义的函数,this指向其运行时绑定的上下文。
作为普通函数被调用时,绑定为全局对象;
作为构造函数(或者说类)被调用时,指向新建的对象。
作为对象方法被调用时,指向调用它的对象。
使用call()、apply()调用,以及bind()方法返回的函数调用时,绑定为指定的值,可能是对象,也可能是原始值。对于使用箭头函数方式定义的函数,其this值固定为闭合词法上下文的值,也就是其定义时的上下文环境。因为不能修改this指向,所以箭头函数不能作为构造函数使用。
需要注意的是,如果在对象的成员方法里定义一个嵌套函数,这个嵌套函数的this指向全局对象或调用时指定的值,而不是其定义或运行时所在的对象。这与在普通函数里定义嵌套函数的行为一致。
-
在严格模式下
两种方式调用函数如果作为普通函数调用,其this值都固定为undefined,而不是全局对象。
以其余方式调用的话,它们的this值指向规则与非严格模式一样。
函数的属性与方法
Function.prototype.length
只读属性,表示函数的元数,即声明函数时指定的形参个数。
Function.prototype.name
只读属性,函数名
Function.prototype.prototype
除了箭头函数,其他函数都有prototype属性。指向一个被叫做原型的对象,用于该继承该对象的属性。应该在作为构造函数使用它。
Function.prototype.call() | apply() | bind()
用于调用函数
function.call(thisArg[, arg1[, arg2[, ...]]])
function.apply(thisArg[, argsArray])
let proxyFn = function.bind(thisArg[, arg1[, arg2[, ...]]]);
proxyFn([, arg1[, arg2[, ...]]]);
Function.prototype.toString()
默认返回声明函数的完整代码
Function构造函数
所有函数对象的类。
用于使用字符串参数创建一个函数对象,前面的参数是形参的名字,最后一个参数是函数体的代码。
const f = new Function("x", "y", "return x+y;");
// 相当于
const f = function(x, y) {return x+y;}
在跨域脚本使用Function构造函数,需要在内容安全策略配置unsafe eval
才能使用。
函数式编程
函数式编程是一种编程范式,有三个原则:
- 不改变数据(无副作用)
即不会修改函数外面的数据 - 使用纯函数
要求固定的输入一定得到固定的输出 - 使用表达式而不是语句
表达式是一个运算过程,是会产生值的,而语句指得是执行某个没有返回值的操作。
这意味这函数是编程要求函数体内每一步都是单纯的运算,且都有返回值。
高阶函数
就是操作函数的函数。接收一个或多个函数作为参数并返回一个新的函数。
函数参数的部分应用
在使用bind(),call(),apply()方法调用函数时,我们传入的参数会被放到函数参数列表的开头,称之为部分应用参数。
function fn (a,b,,c) {
condole.log(a,b,c);
}
function partialLeft(fn, ...args) {
return function (...args1) {
return fn.call(this, ...args, ...args1); // 'a b undefined'
}
}
将参数应用到右侧
function partialRight(fn, ...args) {
return function (...args1) {
return fn.call(this, ...args1, ...args);
}
}
函数记忆
除了使用函数对象的属性,还可以使用高阶函数与闭包特性实现数据的缓存。