JS学习笔记之再理解一等公民--函数(基础篇)

声明函数的方式

这里其实我比较迷惑,我以前认为声明函数只有函数声明方式和函数表达式,其它的所有情况比如在类里面的,对象里面的都归于这两个,最近看资料又觉得其它方式可以单独成为一种声明函数的方式,所以跑回来完善了一下文章。

方式1. 函数声明(Function declartion)

function 函数名([形参列表]) { 
    //函数体 
}

函数声明会被提升到作用域顶部,也就是说,你可以在某个函数声明前调用它而不会报错。
函数声明的函数名是必须的,所以它有name属性。


方式2. 函数表达式(Function expression)

let 变量名 = function [函数名]([形参列表]) { 
    //函数体 
}

在某个对象中的函数表达式:

const obj = {
  sum: function [函数名]([形参列表]) {
    //函数体
  }
}

函数表达式又分为具名函数和匿名函数,以上,如果有“函数名”就是具名函数,反之是匿名函数。

对于具名函数,函数名的作用域只在函数内部,而变量名的作用域是全局的,所以在函数内部即可以使用函数名也可以使用变量名调用自身,在函数外部则只能使用变量名调用。

//函数表达式--具名函数
let factorial = function fact(x) {
    if (x <= 1)  return 1;
    else         return x * fact(x-1);//正确
    //else         return x * factorial(x-1);//正确
}
factorial(5); //正确
fact(5); //错误

具名函数有name属性,匿名函数没有。

推荐使用具名函数,原因如下:

  1. 具名函数有更详细的错误信息和调用堆栈信息,更方便调试
  2. 当在函数内部有递归调用时,使用函数名调用比使用变量名调用效率更高

函数表达式不会被提升到作用域顶部,原因是函数表达式是将函数赋值给一个变量,而js对提升变量的操作是只提升变量的声明而不会提升变量的赋值,所以不能在某个函数表达式之前调用它。


注意

1. 函数表达式可以出现在任何地方,函数声明不能出现在循环、条件判断、try/catch、with语句中。

注:只有在严格模式下,在块语句中使用了函数声明才会报错。

2. 立即执行函数只能是函数表达式而不能是函数声明,但使用函数声明不会报错,只是不会执行
例2:

//函数声明方式
function square(a){
    console.log(a * a);
}(5)
//函数表达式方式
let square = function(a){
    console.log(a * a);
}(5)
//错误的方式
function(a){
    console.log(a * a);
}(5)

上面的代码第一段不会打印出值,第二段能打印出值,出现这种区别的原因是只有函数声明可以提升,函数声明后面的()直接被忽略掉了,所以它不能立即执行。而第三段代码会报错,因为它既没有函数名又没有赋值给变量,js引擎就会将其解析成函数声明。为了避免在某些情况下js解析函数产生歧义,js建议在立即执行函数的函数体外面加一对圆括号:
例3:

(function square(a){
    console.log(a * a) ;
}(5))
(function(a){
    console.log(a * a) ;
}(5))

上面的代码都可以正常执行了,js会将其正确解析成函数表达式。


方式3. 速记方法定义(Shorthand method definition)

在对象里:

const obj = {
  函数名([形参列表]) {
    //函数体
  }
}

在类里面(React里面就是这种方式):

class Person {
  constructor() {}
  函数名([形参列表]) {
    //函数体
  }
}

这种方式定义的方法是具名函数。
比起 const obj = {add: function() {} } ,更推荐这种方式。


方式4. 箭头函数(Arrow function)

const 变量名 = (形参列表) => {
  //函数体
}

箭头函数的特点:

  1. 箭头函数没有自己的执行上下文(execution context), 也就是,它没有自己的this.
  2. 它是匿名函数
  3. 箭头内部也没有arguments对象

方式5. 函数构造函数(function constructor)

在js中,每个函数实际都是一个Function对象,而Function对象是由Function构造函数创建的。

const 变量名 = new Function([字符串形式的参数列表],字符串形式的函数体)

比如:

const adder = new Function("a", "b", "return a + b")

完全不推荐使用这种方式,原因如下:

  1. Function对象是在函数创建时解析的,这比函数声明和函数表达式更低效。
  2. 不论在哪里用这种方式声明函数,它都是在全局作用域中被创建,所以它不能形成闭包。

调用函数的方式

四种方式:

  1. 作为函数
    作为函数的意思就是在全局作用域下、某个函数体内部或者某个块语句内部调用
    当以此方式调用函数时,一般不会使用到this关键字(这也是它和作为方法调用时的最大区别),因为此时的this要么指向全局对象window(非严格模式下)要么为undefined(严格模式下)
let sayHello = function(name) {
  console.log(`hello ${name}`)
}
sayHello('melody')
  1. 作为方法
    作为方法的意思就是函数作为一个对象里的属性被调用,此时函数的this指向该对象,并且函数可以访问到该对象的所有属性。
let person = {
  name: 'melody',
  sayHello: function() {
     console.log(`hello ${this.name}`)
  }
}
person.sayHello() // hello melody

Note: 这种方式要注意this 可能会改变的情况

let _sayHello = person.sayHello
// 此时的this指向的对象已经变成了`window`而不是`person`,`this.name`的值为`undefined`
_sayHello() // hello undefined
  1. 作为构造函数
    作为构造函数调用时,this指向构造函数的实例
function Person(name, age) {
    this.name = name
    this.savor = age
}

    let person1 = new Person('melody', 'sleeping')
    let person2 = new Person('shelly', 'singing')
  1. 使用call(),apply()或者bind()方法
    这三个方法都是可以显示指定this的指向的,即任何函数都可以作为任何对象的方法来调用

这四种方式最大的不同就是this的指向问题,首先,作为函数调用的this是最好理解的,而作为方法调用看起来也不难,无非就是方法是哪个对象的属性this就指向谁嘛,但两个结合起来可能就比较容易迷惑人:
例4:

let obj = {
    name: 'melody',
    age: 18,

    sayHello: function() { //sayHello()是obj对象的属性
        console.log(this.name);
        sayAge();
        function sayAge() { //sayAge()是sayHello()的内部函数
            console.log(this.age)
        }
    }
}
obj.sayHello();

首先,sayHello()方法定义在obj对象上,那么sayHello()里面的this就指向了obj,所以第一个会打印出melody,接着sayHello()调用了它的内部函数sayAge(),此时sayAge()里面的this.age应该是什么?是obj对象上的age吗?其实不是,在sayAge()里面打印出this会发现this是指向window对象的,所以第二个console会打印出undefined

因为这时候外面多了一个对象,我们就容易被这个对象迷惑,以为嵌套函数的this和外层函数的this的指向是一样的,而其实此时我们遵循的原则应该是第一条:当作为函数调用时,this要么指向全局对象window(非严格模式下)要么为undefined(严格模式下),也就是外层函数是作为方法调用,而嵌套函数依然是作为函数调用的,它们各自遵循各自的规则。如果想让嵌套函数和外层函数的this都指向同一个,以前的方法是将this的值保存在一个变量里面:

...
    sayHello: function() {
        let that = this;
        function sayAge() {
            console.log(that.age) //18
        }
    }
...

或者使用ES6新增的箭头函数:

...
    sayHello: function() {
        console.log(this.name); //melody
        let sayAge = () => {
            console.log(this.age) //18
        }
        sayAge();
    }
...

关于箭头函数和普通函数的this的区别,后面再详细讲吧~

作为构造函数就很强了,这就涉及到js里面最难也最重要到部分:原型和继承,它们重要到这篇文章都没资格展开,所以就略过吧~嗯...我的意思是下一次总结。

call(),apply()和bind()

相同之处:

  • 第一个参数都是指定this的值

不同之处:

  • 从第二个参数开始,call()和bind()是函数的参数列表,apply()是参数数组。
  • call()和apply()是立即调用函数,bind()是创建一个新函数,将绑定的this值传给新函数,但新函数不会立即调用,除非你手动调用它。

举例说明这三个方法的基本用法:
例5:

let color = {
    color: 'yellow',
    getColor: function(name) {
        console.log(`${name} like ${this.color}`);
    }
}
let redColor = {
    color: 'red'
}

color.getColor.call(redColor, 'melody')
color.getColor.apply(redColor, ['melody'])
color.getColor.bind(redColor, 'melody')()

首先,apply()方法的第二个参数是数组,call()和bind()是参数列表,其次,apply()和call()会立即调用函数而bind()不会,所以要想bind()后能立即执行函数,需要在最后加一对括号。

apply()和call()
前面也说了,这两个函数的唯一区别就是第二个参数的格式,apply()的第二个参数是数组,call()从第二个参数开始是函数的参数列表,并且参数顺序需要和函数的参数顺序一致,如下:

let obj = {}; //模拟this
function fn(arg1,arg2) {}
//调用
fn.call(obj, arg1, arg2);
fn.apply(obj, [arg1, arg2]);

注意:目前的主流浏览器几乎都支持apply()方法的第二个参数是类数组对象,我在Chrome, Firefox, Opera, Safari上面都测试过,只要是类数组对象就可以,不过低版本可能会不支持,所以建议先将类数组转换成数组再传给apply()方法。

用法一:类数组对象借用数组方法
常见的类数组对象有:

  • arguments对象,
  • getElementsByTagName(), getElementsByClassName (), getElementsByName(), querySelectorAll()方法获取到的节点列表。

注:类数组对象就是拥有length属性的特殊对象

例6:将类数组对象转换成数组

Array.prototype.slice.call(arguments);
[].slice.call(arguments);
//或者
Array.prototype.slice.apply(arguments);
[].slice.apply(arguments);

因为此时不需要给slice()方法传入参数,所以call()apply()都可以实现。

例7:借用其它数组方法

//类数组对象
let objLikeArr = {'0': 'melody','1': 18,'2': 'sleep',length: 3}

//借用数组的indexOf()方法
Array.prototype.indexOf.call(objLikeArr, 18); //1
Array.prototype.indexOf.apply(objLikeArr, ['sleep']); //2

用法二:求数组最大(小)值
Math.max()Math.min()可以找出一组数字中的最大(小)值,但是当参数为数组时,结果是NaN,这时候用apply()方法可以解决这个问题,因为apply()的第二个参数接收的是数组。
例8:

let arr1 = [1,2,12,8,9,34];
Math.max.apply(null, arr1); //34

数字字符串也可以:
例9:

let a = '1221679183';
Math.max.apply(null, a.split('')); //9

用法三:借用toString()方法判断数据类型
这不是最好用的判断数据类型的方法,但是是最有效的方法。
例10:

//基本数据类型
    let null1 = null;
    let undefined1 = undefined;
    let str = "hello";
    let num = 123;
    let bool = true;
    let symbol = Symbol("hello");

//引用数据类型
    let obj = {};
    let arr = [];
    let fun = function() {};
    let reg = new RegExp(/a+b/, 'g');
    let date = new Date();


    Object.prototype.toString.call(null1) //[object Null]
    Object.prototype.toString.call(undefined1) //[object Undefined]
    Object.prototype.toString.call(str) //[object String]
    Object.prototype.toString.call(num) //[object Number]
    Object.prototype.toString.call(bool) //[object Boolean]
    Object.prototype.toString.call(symbol) //[object Symbol]

    Object.prototype.toString.call(obj) //[object Object]
    Object.prototype.toString.call(arr) //[object Array]
    Object.prototype.toString.call(fun) //[object Function]
    Object.prototype.toString.call(reg) //[object RegExp]
    Object.prototype.toString.call(date) //[object Date]

用法四:实现函数不定参
一个常见的用法是实现console可接收多个参数的功能:
例11:

function log() {
    console.log.apply(console, arguments)
}
log('hello'); //hello
log('hello', 'melody'); // hello melody

es6新增的 ... 运算符其实更方便:

function log(...arg) {
    console.log(...arg);
}

还可以加默认的打印值:

    function logToHello() {
        let args = Array.prototype.slice.call(arguments);
        args.unshift('(melody say)');
        console.log.apply(console, args)
    }

    logToHello('thank you.', 'I hope you have a good day');
    logToHello('thank you.');

bind()

bind() 函数会创建一个新函数,称为绑定函数,绑定函数与原函数具有相同的函数体。当绑定函数被调用时 this 值绑定到 bind() 的第一个参数,并且该参数不能被重写,也就是绑定的this就不再改变了。

用法一:解决将方法赋值给另一个变量时this指向改变的问题
当函数作为对象的属性被调用时,如果这时候是先将方法赋值给一个变量,再通过这个变量来调用方法,此时this的指向就会发生变化,不再是原来的对象了,这时候,就算该函数使用箭头函数的写法也无济于事了。解决方法是在赋值时使用bind()方法绑定this。:
例12:

name = "Tiya"; //全局作用域的变量
let obj1 = {
    name: 'melody', //局部作用域的变量
    sayHello: function() { 
        console.log(this.name);
    },
}
let sayHello1 = obj1.sayHello;
sayHello1() //Tiya,this的指向发生了变化,指向全局作用域

let sayHello = obj1.sayHello.bind(obj1);
sayHello() //melody

用法二:解决dom元素上绑定事件,当事件触发时this指向改变的问题
这个问题最常出现在使用某些框架的时候,比如React,写过React的小伙伴肯定对于this.xxx.bind(this)这种写法再熟悉不过了,因为React内部并没有帮我们绑定好this,所以需要我们手动绑定this,否则就会出错。
例13:

//模拟的dom元素
<div id="container"></div>

let ele =  document.getElementById("container");
let user = {
    data: {
        name: "melody",
    },
    clickHandler: function() {
        ele.innerHTML = this.data.name;
    }
}

ele.addEventListener("click", user.clickHandler);  //报错 Cannot read property 'name' of undefined

我们在一个dom元素上监听了点击事件,当该事件触发时,将user对象上的一个变量值显示在该元素上,但如果直接使用ele.addEventListener("click", user.clickHandler),此时,clickHandler事件内部的this已经变成了<div id="container"></div>这个节点而不再是user本身了,正确的做法是调用时给clickHandler绑定this

ele.addEventListener("click", user.clickHandler.bind(user));

实参、形参和arguments对象

简单来说,形参是声明函数时的参数,实参是调用函数时传入的参数。
例14:

function getName(name) { //此处为形参
    console.log(`my name is ${name}`);
}
getName('melody'); //此处为实参

js的函数,调用时传入的参数和声明时的参数个数可以不一致,类型可以不一致(也没有声明类型的机会),这就是为什么js没有函数重载概念的原因。
情况一:实参数量 >形参数量
此时函数会忽略多余的实参,就比如说前面的例子:

function log(name) {
    console.log(name);
}
log('world', 'hello'); //world

情况二:实参数量 <形参数量
此时多余的参数的值为undefined,比如:

function log(name, age) {
    console.log(name, age);
}
log('world'); //world undefined

arguments是函数内部可以获取到传入的参数的类数组对象,要注意的是arguments的长度代表的是实参的数量,而不是形参的数量。

前面说到js没有函数重载的概念,但可以用arguments对象模拟函数的重载:

function overloading() {
    switch(arguments.length) {
        case 1:
            return arguments[0];
            break;
        case 2:
            return arguments[0] + arguments[1];
            break;
        default:
            return 0;
            break;
    }
}

es6以后,js慢慢有了比arguments更好的方式去处理函数的参数,比如rest参数,前面的例子也提到过:

function log(...arg) {
    console.log(...arg);
}
log(1,2)

它看起来比arguments更容易理解也更简洁,js应该也有想淘汰arguments的想法,所以建议大家能用es6语法实现的就不要用arguments了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270