函数相关知识点补充

记录一些初学函数的一些知识点/不太常用的函数相关知识点

函数是对象

  • JavaScript 函数是通过 function 关键词定义的。JavaScript 中的 typeof 运算符会为函数返回 “function”。但是最好是把 JavaScript 函数描述为对象
  • JavaScript 函数都有属性和方法

函数的对象属性arguments.length

返回函数被调用时收到的参数数目

1
2
3
function myFunction(a, b) {
return arguments.length; // 2
}

函数的对象方法toString()

字符串形式返回函数:

1
2
3
4
5
function myFunction(a, b) {
return a * b;
}

var txt = myFunction.toString(); // 'function myFunction(a, b) {\n return a * b;\n}'

函数声明与函数表达式

函数声明

  • 被声明的函数不会直接执行。它们被“保存供稍后使用”,当它们被调用时执行。
  • 无法对函数声明进行自调用
  • 一个名为multiply的函数声明:
    1
    2
    3
    function multiply(x, y) {
    return x * y;
    } // 没有分号

函数表达式(匿名函数)

  • JavaScript 函数也可以用表达式来定义
  • 函数表达式可以在变量中存储
  • 在变量中保存函数表达式之后,此变量可用作函数.
  • 存放在变量中的函数不需要函数名。他们总是使用变量名调用。
  • 注意:下面这是一个 匿名函数 的函数表达式,被赋值给变量multiply:
    1
    2
    3
    4
    // 注意:这算是匿名函数!!只是赋值给变量multiply了
    var multiply = function(x, y) {
    return x * y;
    };
  • 一个命名为func_named的函数的函数表达式,被赋值给变量multiply:
    1
    2
    3
    var multiply = function func_name(x, y) {
    return x * y;
    };
    注意:在函数外部无法通过 func_name 访问到函数,因为这已经变成了一个表达式。

自调用函数

  • 函数表达式可以自调用
    • 注意:自调用函数(即 自执行函数)一般都是匿名函数,而函数声明不能定义匿名函数!!所以无法对 函数声明 进行自调用
    • 补充:ES6提供了一种函数作为对象属性的简写方法(const a = { b(){...} }),实际上也是函数声明的定义方式(相当于const a = { say: function(){...} })(可参考《ES6扩展 对象扩展》的“简洁表示法(简写对象属性、方法)”
  • 在不进行调用的情况下,自调用表达式是自动被调用(开始)的。
  • 假如表达式后面跟着(),则函数表达式会自动执行。

多种写法

  • 写法1【推荐,jslint推荐写法】:最外层加括号。需要在函数最外添加括号,以指示它是一个函数表达式,否则会报错
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 写法1【推荐,jslint推荐写法】
    (function () {
    console.log('Hello');
    }());
    (function (形参1, 形参2) {
    console.log('Hello');
    }(实参1, 实参2));

    /*
    易错情况1:function () {console.log('Hello');}被当成了函数声明,所以需要函数名
    并且末尾的()变成孤立部分
    解决方法:使函数声明变为函数表达式,即 在函数最外添加括号(例子:写法1)
    */
    function () {
    console.log('Hello');
    }() // 报错 Uncaught SyntaxError: Function statements require a function name

    // 易错情况2:
    var a = 90 // 这里不写分号会报错 Uncaught TypeError: 90 is not a function
    (function () {
    console.log('Hello');
    }());
  • 写法2:function外层加括号
    1
    2
    3
    4
    5
    6
    7
    // 写法2
    (function () {
    console.log('Hello'); //我会调用我自己
    })();
    (function (形参1, 形参2) {
    函数体 //我会调用我自己
    })(实参1, 实参2);
  • 写法3:function前加一元运算符,常用!/-/+
    1
    2
    3
    4
    // 写法3
    !function () {
    console.log('Hello'); //我会调用我自己
    }();
  • ! function (){}() 表示该函数是一个函数表达式而不是函数声明,前面的可以理解成它是将函数声明转化成函数表达式的一种方法!function (){}()也等同于(function (){})();也可换成+-这样的一元运算符,都可以起到相同的作用。这些运算符会将后面的函数体当成一个整体,先对匿名函数进行求值,然后在对结果进行运算。

变量与函数提升

  • 关于提升MDN
  • 总结:(具体见下面例子)
    • ES6不存在 变量提升,但存在函数提升
      • 只有var命令声明的变量会发生 变量提升,ES6的let则不会
      • 变量声明会被提升到 当前作用域 的头部
        • 值的初始化还是保留在原位,不会提升
      • ES6之前只有 全局作用域 和 函数作用域,这两个作用域都存在 变量提升let命令的块级作用域不存在变量提升
        • 变量提升 只存在 全局/函数作用域 中,**if(){}for(){}不是函数作用域,他们里面的变量会提升到包裹他们的全局/函数作用域的顶部**。
    • ES5中是 整个函数被提升,而ES6只提升 函数的声明,函数内容是不会被提升的,他们将被留在原来的位置。
      • 这导致了JavaScript 函数能够在声明之前被调用,不过会undefined,还是建议规范书写代码。
      • ES5是提升到 全局/函数作用域 顶部,ES6是提升到 块级作用域 顶部
      • 通过函数声明方式创建的函数会被提升,通过表达式方法创建的函数不会被提升帮助理解
    • 函数和变量相比,会被优先提升。这意味着函数会被提升到更靠前的位置。
  • 补充:当出现多个同名变量与同名函数时,调用该变量名时的优先级为:变量声明< 函数声明 < 变量赋值(具体参考牛客网20191215第7题)

变量提升

  • ES6之前只有 全局作用域 和 函数作用域,这两个作用域都存在变量提升
  • var命令声明的变量,不管在什么位置,变量声明都会被提升到当前作用域的头部
    • 变量的提升只会对var命令声明的变量有效,其他不是用var命令声明的变量,不会发生变量的提升。
  • ES6中取代varlet命令的作用域限定在块级,使用**let声明的变量不存在变量提升**。
  • 注意:变量声明被提升到了当前作用域的头部, ES6之前只有全局作用域和函数作用域if不是函数作用域,所以变量tmp提升到了foo函数的头部。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function foo(x) {
    if (x > 100) {
    var tmp = x - 100;
    }
    }

    // 等同于
    function foo(x) {
    var tmp; // 注意:提升到了if的外面
    if (x > 100) {
    tmp = x - 100;
    };

函数提升

通过 函数声明方式 创建的函数会被提升,通过 表达式方式 创建的函数 不会 被提升原因

例子
我们正常书写的代码:

1
2
3
4
5
6
7
8
9
10
function f() { console.log('I am outside!'); }

(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}

f();
}());

实际在内存中被函数提升以后:

1
2
3
4
5
6
7
8
9
// ES5 环境
function f() { console.log('I am outside!'); }

(function () {
function f() { console.log('I am inside!'); }
if (false) {
}
f();
}());

ES5中,在if内声明的函数f会整个被提升到函数作用域顶部。

1
2
3
4
5
6
7
8
9
10
11
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;//函数f的声明提升
if (false) {
function f() { console.log('I am inside!'); }
}

f();
}());
// Uncaught TypeError: f is not a function

而在ES6中,块级作用域内声明的函数,行为类似于var声明的变量(只提升声明)。函数f是在块级作用域内被声明的,因此 函数f的声明 被提升到了 块级作用域 的顶部


箭头函数

  • IE11 或更早的版本不支持箭头函数。
  • 箭头函数允许使用简短的语法来编写函数表达式。
  • 不需要 function 关键字、return 关键字和花括号。
    1
    2
    3
    4
    5
    6
    7
    // ES5
    var x = function(x, y) {
    return x * y;
    }

    // ES6
    const x = (x, y) => x * y;
  • 箭头函数没有自己的 this。它们不适合定义对象方法。(例子在下方“作为对象属性的简写”中)
  • 箭头函数未被提升。它们必须在使用前进行定义
  • 使用 const 比使用 var 更安全,因为函数表达式始终是常量值。
  • 如果函数是单个语句,则只能省略 return 关键字和大括号。因此,保留它们可能是一个好习惯:
    1
    const x = (x, y) => { return x * y };

返回对象需要注意

  • 如果要返回一个对象,就要注意,如果是单表达式,这么写的话会报错:
    1
    2
    // SyntaxError:
    x => { foo: x }
  • 因为和函数体的{ ... }有语法冲突,所以要改为:
    1
    2
    // ok:
    x => ({ foo: x })
  • 可参考廖雪峰的博客

箭头函数的this指向

  • MDN解释的十分详细易懂
  • 可以复习下this指向
  • 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this
    • 比如:如果该函数是一个构造函数,this指向一个新的对象
    • 严格模式下的函数中,this 指向undefined
    • 如果该函数是一个对象的方法,则它的 this 指向这个对象
      • 注意:有一种情况对象属性是方法时this 指向这个对象(见下面“作为对象属性的简写”obj1例子)
  • 因此,在下面的代码中,传递给setInterval的函数内的this与封闭函数中的this值相同:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Person(){
    // Person() 构造函数定义 `this`作为它自己的实例。
    this.age = 0;

    setInterval(() => {
    this.age++; // `this` 正确地指向 p 实例
    }, 1000);
    }

    var p = new Person();

回调函数中的this

  • 回调函数中的this默认是指向windows的,因为本质上是在函数内callback,并没有.前的对象调用
  • 注意:回调函数和箭头函数不同,回调函数时指向windows,而箭头函数只是指向定义他的对象外层的this,不一定是windows
  • 也就是说,如果回调函数中想要使用this有两种解决方法(如下)
    • 闭包获取:要么在使用回调函数之前利用闭包的特性先使用一个变量_this保存this
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      const xiaoming = {
      name:"xiaoming",
      age: null,
      getAge: function () {
      let _this = this;
      //...ajax
      setTimeout(function () {
      _this.age = 15;
      console.log(_this);
      }, 1000);
      }
      };
      xiaoming.getAge();//{name: "xiaoming", age: 15, getAge: ƒ}
    • 箭头函数获取:要么将回调函数改为箭头函数,这样this取到外层的this,即我们需要的对象xiaoming:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      const xiaoming = {
      name: "xiaoming",
      age: null,
      getAge: function () {
      //...ajax
      setTimeout(() => {
      this.age = 15;
      console.log(this);
      }, 1000);
      }
      };
      xiaoming.getAge();//{name: "xiaoming", age: 15, getAge: ƒ}

作为对象属性的简写

  • 箭头函数在对象中作为对象属性值时的书写方法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const obj1 = {
    name: 'hlz',
    say: () => { // this指向容易导致问题,因为箭头函数不是谁调用它this就指向谁
    // 所以this在非严格模式下指向window,严格模式下则是undefined
    console.log(this === window) // true
    console.log('名字:', this.name)
    },
    }
    console.log(this === window) // true
  • 回顾下ES6给出的简写方法
    1
    2
    3
    4
    5
    6
    7
    const obj2 = {
    name: 'hlz',
    say() { // 相当于say2: function(){},简写可以省略冒号与function
    console.log(this === obj2) // true
    console.log('名字:', this.name);
    }
    }
  • 他俩的区别体现在this的指向上,前者this指向window(**注意:并不是指向外层obj**),后者this指向调用obj对象:
    1
    2
    3
    this.name = '11' // 或者window.name = '11'
    obj1.say() // 名字: 11
    obj2.say() // 名字: hlz
  • 箭头函数没有自己的 this不适合定义对象方法。比如这里个例子里obj1this指向window,而不是定义时期望的obj1,所以一般我们使用ES6提供的对象属性简写方法(见obj2)
  • 特殊情况:当你期望this指向最外层对象时,可以使用obj1的方法来写,比如在vuemethods中:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    methods: {
    init() { // init()中this指向vue实例
    return {
    a(){
    console.log(this) // 此时this指向init对象{a: ƒ, b: ƒ}
    },
    b: () => {
    // 这种情况使用箭头函数来定义更加合适,毕竟我们期望this指向vue实例
    console.log(this) // this指向vue实例!
    },
    }
    }
    }
    // 所以一般在methods对象的第一层对象属性中,我们使用ES6提供的常规简写方法。
    // 而在第一层对象的属性值的对象属性中,我们使用箭头函数来写,确保this指向vue实例

与严格模式的关系

  • 鉴于 this 是词法层面上的,严格模式中与 this 相关的规则都将被忽略。
    • 严格模式下的this相对于非严格模式下的this的主要区别在于:对于JS代码中没有写执行主体(对象)的情况下,非严格模式默认都是window执行的,所以this指向的是window,但是在严格模式下,没有写执行主体,this指向是undefined
  • 语法:'use strict'; 为函数开启严格模式的语法
    1
    2
    3
    4
    var f = () => { 
    'use strict'; // 为函数开启严格模式的语法
    return this;
    };

call apply bind 并不会影响其 this 的指向

  • 需要了解:
    • callapplybind 这些函数可以用于显式地改变函数的 this 指向
    • 普通函数的 this 指向可以改变的原因: 普通函数的 this 是在函数调用时动态确定的,所以可以通过callapplybind 这些函数改变 this 指向
  • 这些函数对箭头函数的 this 指向没有影响的原因: 箭头函数有一个固定的 this,它是由定义时的上下文决定的,所以无法通过这些函数来改变
  • 由于 箭头函数没有自己的this指针,所有通过 call()/apply()/bind() 方法调用一个函数时,只能传递参数,不能绑定this,即 他们的第一个参数会被忽略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const obj = {
value: 42,
};

function normalFunction() {
console.log(this.value);
}

const arrowFunction = () => {
console.log(this.value);
};

normalFunction.call(obj); // 输出:42,通过 call 改变普通函数的 this 指向
arrowFunction.call(obj); // 输出:undefined,箭头函数的 this 不受 call 影响

const boundFunction = arrowFunction.bind(obj);
boundFunction(); // 输出:undefined,bind 也无法改变箭头函数的 this 指向
  • 在这个例子中,normalFunction 是一个普通函数,我们使用 call 来显式地改变它的 this 指向,使它指向了 obj,所以可以正常输出 42。然而,当我们尝试使用 callbind 来改变箭头函数 arrowFunctionthis 指向时,它依然会继承外围函数(这里是全局作用域)的 this,所以输出仍然是 undefined

return

  • 当在函数体中使用return语句时,函数将会停止执行
  • 如果指定一个值,则这个值返回给函数调用者
  • 如果省略该值,则**返回undefined**。
  • 返回多个值可以放到对象中:
    1
    2
    3
    4
    return {
    aa:20,
    bb:30
    }
  • return是关键字,不是函数,其后面的括号不是必须的,加括号易于阅读代码。
  • 可参考MDN

例子

1
2
3
4
5
function square(x) {
return x * x;
}
var demo = square(3);
// demo will equal 9

HTML事件绑定函数时不加括号

  • 加括号就是直接执行函数了,不加括号则是指向函数地址,当事件发生时才去调用函数。
  • 事件=function(){}或者事件=函数名都是可以的,不能使用事件=(function(){})(参数)事件=函数名()

事件绑定函数传参

如果事件绑定函数想要传参可以将传参的函数放到另一个匿名函数中,事件绑定该匿名函数。

例子
想要使用onclick事件绑定一个想传参的函数。我们将传参的函数放入匿名函数中,再让onclick事件绑定该匿名函数:

1
xx.onclick=function(){函数名(参数);}

或者

1
xx.onclick= ()=>{函数名(参数)}

在这里不能直接绑定函数名(参数)传参是因为函数名(参数)直接执行了函数,而onClick需要绑定的事件函数是不能立即执行的,所以需要新建一个 箭头函数/匿名函数 让他不立即执行。

区分react和html中的事件写法

  • react中是onClick
  • html中是onclick