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);
  }
}

函数记忆

除了使用函数对象的属性,还可以使用高阶函数与闭包特性实现数据的缓存。

posted @ 2023-05-19 16:39  钰琪  阅读(20)  评论(0编辑  收藏  举报