JavaScript基础--一文认识函数

一、前言

记得最开始学前端的时候,在JavaScript中学的第一类知识就是函数了...这里将学习的感觉分享出来!

说明: 函数其实是一种特殊的对象,每一个函数都是Function的实例,实例存在自己的属性和方法,其次由于函数是对象,那么函数名就是获取函数对象的一种方式了,函数通常以函数声明的方式定义,当然也有函数表达式,几乎是等价的,这两种是最常见的,还存在一种使用Function构造函数来实例化,接收多个字符串参数,最后一个字符串参数作为函数体,前面的所有字符串参数会作为函数的参数

js 复制代码
// 函数声明
function sum(num1, num2) {
    return num1 + num2;
}
js 复制代码
// 函数表达式
let sum = function(num1, num2) { 
    return num1 + num2; 
};
js 复制代码
// Function构造函数
let sum = new Function("num1", "num2", "return num1 + num2");

二、箭头函数

说明: 这是ECMAScript6新增的行为,能够使用=>来定义函数表达式,任何可以使用函数表达式的地方,都可以使用箭头函数,=>后的{}表示包含函数体,可以在一个函数中包含多条语句,跟常规的函数一样,其规定的写法是() => {}

js 复制代码
// 基本使用
let arrowSum = (a, b) => {
  return a + b;
};

let functionExpressionSum = function (a, b) {
  return a + b;
};

console.log(arrowSum(5, 8));
console.log(functionExpressionSum(5, 8));
js 复制代码
// 作为嵌套函数使用
let ints = [1, 2, 3];

console.log(
  ints.map(function (i) {
    return i + 1;
  })
);

console.log(
  ints.map((i) => {
    return i + 1;
  })
);

注意:

  • 如果只有一个参数,可以省略()
  • 如果=>后面只有一句代码时,可以同时省略{}和return
  • 箭头函数不存在自己的this,它的this与其父作用域的this指向相同
  • 在只有一个参数的时候如果有使用默认值,则()不能省略
js 复制代码
// 只有一个参数的时候,=>后只有一句代码
let triple = x => return 3 * x;

三、函数名

说明: 因为函数名是指向函数的一种方式,如果其它的变量也包含指向这个函数的方式,那它们的行为是相同的,也就是说一个函数可以存在多个名称,这个名称可以通过name属性获取,属性值为保存的变量名,如果是匿名函数则值为"",如果是通过Function实例生成,则属性值为anonymous

js 复制代码
function sum(num1, num2) {
  return num1 + num2;
}
let anotherSum = sum;

console.log(sum(10, 10));
console.log(anotherSum(10, 10));

由于函数名在这里可以看作变量,那么函数可以在任何使用变量的地方使用函数,也就是说函数可以当做值,也可以做为值被函数返回

四、参数

说明: ECMAScript函数既不关心传入的参数个数,也不关心这些参数的数据类型, 也就是你在写函数的时候定义了两个参数,不意味着你必须传两个参数,你可以传一个,也可以一个不传,编辑器都不会报错,因为参数在函数内部表现为一个数组在使用 function关键字定义(非箭头)函数时,可以在函数内部访问arguments对象,从中取得传进来的每个参数值,这个对象是一个类数组对象,有length属性,也可以通过[]访问其内部元素,这个对象里面的值与函数参数是对应

js 复制代码
function sayHi(name,message) { 
 console.log(arguments); 
} 
sayHi("zhangsan","shigetiancai")

注意:

  • () => {}不存在arguments对象
  • 所有参数都是按值传递的,如果参数是一个对象,那么传递的值就是这个对象的引用
  • arguments对象不会反应函数的默认值,只会显示实际传入函数的参数值

五、没有重载

说明: 重载是指可以定义多个同名函数,只要函数的签名不同就可以,由于ECMAScript函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载,其次函数名可以理解为指针,那么写同名函数就会导致后面的会覆盖前面的指针,也就定义不了多个同名函数了

js 复制代码
let addSomeNumber = function (num) {
    return num + 100;
};
addSomeNumber = function (num) {
    return num + 200;
};

let result = addSomeNumber(100); // 300

六、默认值

说明: ECMAScript6之前设置默认值需要使用typeof检测是不是undefined然后通过?:来设置,而ECMAScript6之后可以通过参数 = 值来设置默认值,值默认是undefined,所以不传的作用是一样的,函数的默认值也可以使用调用函数返回的值

  • 函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。
  • 计算默认值的函数 只有在调用函数但未传相应参数时才会被调用
js 复制代码
let romanNumerals = ["I", "II", "III", "IV", "V", "VI"];
let ordinality = 0;

function getNumerals() {
    return romanNumerals[ordinality++];
}

function makeKing(name = "Henry", numerals = getNumerals()) {
    return `King ${name} ${numerals}`;
}

// 不传,会计算默认值
console.log(makeKing());

// 都传,不会得到默认值
console.log(makeKing("Louis", "XVI"));

1.默认值作用域

说明: 给多个参数定义默认值实际上跟使用let关键字顺序声明变量一样,默认参数会按照定义它们的顺序依次被初始化,所以后定义默认值的参数可以引用先定义的参数,但是前面定义的参数不能引用后面定义的,同时参数也存在于自己的作用域中,它们不能引用函数体的作用域

js 复制代码
function makeKing(name = "Henry", numerals = "VIII") {
    return `King ${name} ${numerals}`;
}

<=> 这两个函数效果是一致的

function makeKing() {
    let name = "Henry";
    let numerals = "VIII";
    return `King ${name} ${numerals}`;
}
js 复制代码
// 后定义可以使用先定义
function makeKing(name = "Henry", numerals = name) {
    return `King ${name} ${numerals}`;
}

// 先定义不可以使用后定义
function makeKing(name = numerals, numerals = "VIII") {
    return `King ${name} ${numerals}`;
}
      
// 参数不能使用函数体内的变量
function makeKing(name = "Henry", numerals = defaultNumeral) {
    let defaultNumeral = "VIII";
    return `King ${name} ${numerals}`;
}

七、参数扩展与收集

说明: ECMAScript6新增了...操作符,它可以非常简洁地操作和组合集合数据,主要在函数参数列表中使用,此时既可以用于调用函数时传参,也可以用于定义函数参数

1.扩展参数

说明: 对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入,所以对于那些需要数组中每个元素依次传入的时候很有用,这种情况可以与arguments对象结合使用,不过这个对象并不知道...的存在,只是按照调用函数时传入的参数接收每一个值

js 复制代码
let values = [1, 2, 3, 4]
console.log(...values)
js 复制代码
let values = [1, 2, 3, 4];
function countArguments() {
    console.log(arguments.length);
}

countArguments(-1, ...values);
countArguments(...values, 5);
countArguments(-1, ...values, 5);
countArguments(...values, ...[5, 6, 7]);

2.收集参数

说明: 在定义函数的时候,可以使用...操作符把不同长度的独立参数组合为一个真实数组,收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得到空数组。因为收集参数的结果可变,所以只能把它作为最后一个参数,它的使用与arguments对象的使用不冲突

js 复制代码
function getSum(...values) {
    return values;
}

console.log(getSum(1, 2, 3));
js 复制代码
// 收集参数只能作为最后一个参数
function getProduct(...values, lastValue) {}

function ignoreFirst(firstValue, ...values) {
    console.log(values);
}

ignoreFirst();
ignoreFirst(1);
ignoreFirst(1,2);
ignoreFirst(1,2,3);

八、函数声明与函数表达式

1.函数声明提升

说明: 这是函数声明的函数的特点,也就是函数声明会在代码执行之前获得定义。这意味着函数声明可以出现在调用它的代码之后

js 复制代码
console.log(sum(10, 10));

function sum(num1, num2) {
    return num1 + num2;
}
js 复制代码
// 上面代码在运行的时候应该是这样的,所以不会报错

let sum

console.log(sum(10, 10));

sum = function (num1, num2) {
    return num1 + num2;
}

2.函数表达式

说明: function关键字后面没有标识符的函数称为匿名函数,也就是这个函数没有名字,所以其name属性是"",那么函数表达式可以理解为先创建一个匿名函数,然后将其赋值给一个变量,这种函数不存在声明提升,只有代码运行到的时候才会运行,所以下面这里会报错

js 复制代码
sayHi();
let sayHi = function() {
    console.log("Hi!"); 
};

九、关于函数

1.arguments

说明: 这里介绍一个arguments对象中的callee属性,它是一个指向 arguments对象所在函数的指针,在某些情况下十分有用,看下面这个例子

js 复制代码
// 这是一个递归函数,不过它正确使用的前提是函数名必须是
// factorial,从而导致了紧密耦合
function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * factorial(num - 1);
    }
}
js 复制代码
// 用arguments.callee代替,如此让函数逻辑与函数名解耦,
// 这样无论函数名是什么都可以调用这个函数,而不是将其
// 函数名定为factorial
function factorial(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * arguments.callee(num - 1);
    }
}

2.this

说明: 在标准函数里面,一般谁调用这个函数this指向谁,所以在全局作用域中调用函数,this会指向window

js 复制代码
window.color = "red";
function sayColor() {
    console.log(this.color);
}
// 这里调用相当于window.sayColor(),谁调用指向谁,所以this == window
sayColor();

let o = {
    color: "blue",
    sayColor: function () {
      console.log(this.color);
    },
};

// 这里是对象o调用,this == o,那么o.color就是其内部的属性了
o.sayColor();

3.new.target

说明: 这个属性是为了检测函数是否是通过new关键字创建的,如果不是,则取值为undefined,如果,则值为被调用的构造函数

js 复制代码
function King() {
    if (!new.target) {
        console.log(new.target);
    }
    console.log(new.target);
}
new King();
King();

十、改变函数this指向

说明: 这里有三个方法可以做这件事,分别是:apply(函数内this的值,Array实例 / arguments对象)call(函数内this的值,参数1,参数2...)bind(函数内this的值,参数1,参数2...),它们都可以以指定的this值来调用函数

注意:

  • bind()会创建一个新的函数实例
js 复制代码
window.color = "red";
function sayColor() {
    console.log(this.color);
}

let o = {
    color: "blue",
    sayColor: function () {
      console.log(this.color);
    },
};

sayColor();
sayColor.call(this);
sayColor.call(window);
sayColor.call(o);
js 复制代码
window.color = "red";

var o = {
    color: "blue",
};

function sayColor() {
    console.log(this.color);
}

let objectSayColor = sayColor.bind(o);
objectSayColor();

十一、递归

说明: 递归函数通常的形式是一个函数通过名称调用自己,以求和1+2+3+...+n为例(这里以递归实现)

递归函数的组成部分:

  • 终止条件:它用于终止递归的执行,一般等函数执行到某种条件的时候,递归函数不再调用自身,而是返回一个确定的值,然后从这个确定的值往回推导,推导出自己需要的结果,一般都是当值为0或者1时能够得到一个确定的值
  • 循环体: 用于调用自身的情况,在递归情况下,递归函数通过调用自身并传递更小的输入来解决原始问题的一个或多个子问题,从而将复杂的问题简单化,便于推导结果
  • 缩小范围: 递归函数的输入参数应该朝着基本情况靠近。否则,递归函数可能会进入无限循环,永远无法达到自己规定的终止条件

思路: 按照上面的组成,输入的参数需要缩小范围,那就将1+2+3+...+n变成n+(n-1)+(n-2)+...+1,这样就可以看出输入参数的范围在依次-1,然后每相邻的两项都可以写成n + (n - 1),因为后一项总比前面多1,然后当第n项也就是最后一项(从前往后也就是第一项)的时候,值为1,也就是n === 1,返回的值是1,看图(来自《hello 算法》)

js 复制代码
function recur(n) {
    // 终止条件:也就是第一项的值为1
    if (n === 1) {
        return 1;
    }
    
    // 循环体:每次都会计算 n + (n - 1)的值
    // 范围缩小:由于上面每次都会讲参数 - 1,
    //          这也起到输入参数范围缩小的作用了
    return n + arguments.callee(n - 1);
}

注意: 这里使用arguments.callee而不使用recur是为了递归函数可以赋值给其它变量并且能够继续使用

十二、闭包

说明:一个函数引入了另一个函数作用域中的变量时,这个函数就是一个闭包,其组成如下:

js 复制代码
// 外函数
function fn1() {
    // 数据
    let a = 1;
    // 内函数并返回
    return function fn2() {
        // 在内函数中将数据返回出去
        return a;
    };
}

// 执行闭包函数获取内函数,执行内函数得到返回的数据
let data = fn1();

注意:

  • 闭包函数比普通函数更消耗内存,尽量少的去使用
  • 如果某个闭包不再使用的话,应该将内函数设置为null,使其能够被垃圾回收,避免内存泄露

十三、立即执行函数

说明: 正确名字叫立即调用的函数表达式,也就是这种函数会立即执行,由于其内部是跨级作用域,那么在循环的时候跟使用let定义变量所得到的效果是一致的了,所以常用于锁定参数,函数的格式如下:

js 复制代码
(function () {
    // 块级作用域
})();
js 复制代码
// 假设需要给每个div加上点击事件,在点击的时候确认
// 点击的是第几个,就可以使用这种函数了
let divs = document.querySelectorAll("div");
for (var i = 0; i < divs.length; ++i) {
    divs[i].addEventListener(
      "click",
      (function (frozenCounter) {
        return function () {
          console.log(frozenCounter);
        };
      })(i)
    );
}

十四、私有变量

说明: 任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的变量,不过可以通过闭包来创建访问这些变量的方法,这个方法叫特权方法,它是能够访问函数私有变量(及私有函数)的公有方法,在对象上创建有两种方式:构造函数创建私有作用域创建

1.构造函数创建

说明: 把所有私有变量和私有函数都定义在构造函数中。然后,再创建一个能够访问这些私有成员的特权方法。这样做之所以可行,是因为定义在构造函数中的特权方法其实是一个闭包,它具有访问构造函数中定义的所有变量和函数的能力。

js 复制代码
function Person(name) {
    this.getName = function () {
        return name;
    };
    this.setName = function (value) {
        name = value;
    };
}

let person = new Person("Nicholas");
console.log(person.getName()); // 'Nicholas'
person.setName("Greg");
console.log(person.getName()); // 'Greg'

缺点: 每次调用构造函数都会重新创建一套变量和方法

2.静态私有变量

说明: 通过使用私有作用域定义私有变量和函数来实现

js 复制代码
(function () {
    // 私有变量
    let privateVariable = 10;

    // 私有函数
    function privateFunction() {
      return false;
    }

    // 构造函数:
    // 函数表达式不会创建内部函数
    // 不使用关键字声明则这个变量变成全局变量
    MyObject = function () {};

    // 公有和特权方法
    MyObject.prototype.publicMethod = function () {
      privateVariable++;
      return privateFunction();
    };
})();

私有变量和私有函数是由实例共享的。因为特权方法定义 在原型上,所以同样是由实例共享的。特权方法作为一个闭包,始终引用着包含它的作用域,这样所有属性和方法都是共享的,修改一个实例的值,所有实例都会得到更改

相关推荐
Мартин.2 小时前
[Meachines] [Easy] Sea WonderCMS-XSS-RCE+System Monitor 命令注入
前端·xss
昨天;明天。今天。3 小时前
案例-表白墙简单实现
前端·javascript·css
数云界3 小时前
如何在 DAX 中计算多个周期的移动平均线
java·服务器·前端
风清扬_jd3 小时前
Chromium 如何定义一个chrome.settingsPrivate接口给前端调用c++
前端·c++·chrome
安冬的码畜日常3 小时前
【玩转 JS 函数式编程_006】2.2 小试牛刀:用函数式编程(FP)实现事件只触发一次
开发语言·前端·javascript·函数式编程·tdd·fp·jasmine
ChinaDragonDreamer3 小时前
Vite:为什么选 Vite
前端
小御姐@stella3 小时前
Vue 之组件插槽Slot用法(组件间通信一种方式)
前端·javascript·vue.js
GISer_Jing3 小时前
【React】增量传输与渲染
前端·javascript·面试
eHackyd3 小时前
前端知识汇总(持续更新)
前端
万叶学编程6 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js