JS中函数基础知识之查漏补缺(写给小白的学习笔记)
函数
函数是ECMAScript中 最有意思的部分之一, 主要是因为函数实际上是对象.-- 每个函数 都是Function类型的实例,Function也有属性和方法.
因为函数是对象,所以函数名就是指向函数对象的指针.
常用的定义函数的语法:
①函数声明 ②函数表达式 ③箭头函数
function sum (num1,num2) {
return num1 + num2;
}
let sum = function( num1 ,sum2){
return num1 + num2;
}
let sum = ( num1 , num2 ) => {
return num1 + num2;
};
1. 箭头函数
ES6新增了 使用胖箭头 => 定义函数表达式的能力
任何可以使用函数表达式的地方, 都可以使用箭头函数.
箭头函数 简洁的语法 非常适合嵌入函数的场景
let ins = [1,2,3];
console.log( ins.map( function(i){ return i+1;} ) )//[2,3,4]
console.log( ins.map( (i) => { return i+1;} ) )//[2,3,4]
箭头函数的简写 : ① 如果只有一个参数, 那么参数部分的小括号可以省略 ②如果箭头后面只有一行代码,那么可以省略大括号{} 注意省略大括号 会隐式返回这行代码的值
箭头函数嵌入函数 以及它的简写形式:
console.log( ins.map( (i) => { return i+1;} ) )//[2,3,4]
console.log( ins.map( i => i+1; ) )//[2,3,4]
箭头函数虽然语法简洁,但是很多场合不适用.
① 箭头函数不能使用 arguments对象 、super 和 new.target
② 箭头函数不能用作 构造函数
③ 此外 箭头函数也没有 prototype属性.
2. 函数名
因为函数名 就是指向函数的指针 存储的是该函数对象的引用地址. 所以他们和其他包含对象指针的变量 具有相同的行为. 这意味着一个函数可以有多个名称.如下所示
function sum(num1, num2) {
return num1 + num2;
}
console.log(sum(10, 10)); //20
let anotherSum = sum; //注意函数名后面没有小括号 只会访问函数指针 不会执行函数.
console.log(anotherSum(10, 10)); //20
sum = null; //sum 不再指向 函数的存储地址 但是函数对象还在
console.log(anotherSum(10, 10)); //20 //anotherSum 指向函数的存储地址 可以正常使用
3. 理解参数
ECMASript函数的参数 跟大多数其他语言不同, ECMASript函数既不关心 传入的参数的个数 也不关心这些参数的数据类型. 定义函数时 要接受两个参数,并不意味着调用函数时就传两个参数. 调用时 传 0个参数 1 个参数 3个参数 解释器都不会报错. 这是因为ECMAScript函数的参数在内部表现为一个数组. 函数被调用时 总会接收一个数组,但函数并不关心这个数组中包含什么.
事实上, 使用function关键字定义函数时(非箭头函数), 可以在函数内部访问arguments对象从中取得传进来的每个参数值.
arguments对象 是一个类数组对象,(但不是array的实例) ,因此可以使用中括号[] 访问其中的元素.(第一个参数是arguments[0] 第二个参数是arguments[1] ...) 要确定传进来多少个参数,可以访问arguments.length 属性.
function sayhi(name, message){
console.log("hi "+name+","+message);
}
sayhi('Nico','新的一年 升职加薪 !!')
// 把函数重写为 不声明参数 函数还可以正常调用
function sayhello(){
console.log("hello "+arguments[0]+","+arguments[1]);
}
sayhello('WanZi','新的一年 好运爆棚 !!')
arguments对象 可以跟命名参数一起使用. 如上方在函数 sayhi 中使用 arguments[0]和 name,并且 arguments对象的值 会和对应的命名参数值自动同步. 即修改 arguments[0]的值 name的值 也会随之改变, 反之亦然.
箭头函数中的参数
如果函数是使用 箭头语法定义的,那么传给函数的参数 将不能使用arguments关键字访问,只能通过定义的命名参数访问.
function foo () {
console.log( arguments[0]);
}
foo(5); // 5
let bar = () => {
console.log( arguments[0] );
}
bar(5); //ReferenceError: arguments is not defined
虽然箭头函数没有arguments对象 但可以在包装函数中 把arguments 对象提供给 箭头函数。也就是外层函数使用function定义 所以有arguments对象 可以传递给该函数内部的箭头函数使用 这个arguments对象
function foo(){
let bar = () =>{
console.log( arguments[0]);
}
bar();
}
foo(5);//5
注意 ECMAScript 中所有的参数都是按值进行传递的,即变量调用时 参数存储的值 复制给 命名参数. 如果把对象作为参数传递,那么传递的值 就是这个对象的引用地址.(因为 该参数存储值 也只是对象的引用地址) (如下面代码与结果展示)
let name = 'nico';
function changeName(str){
str = str +'happy';
return str;
}
// 函数调用 只是把参数name 存储的值 复制一份给到命名参数 str
console.log(changeName(name));
console.log(name);
let obj = {
a:1,
b:2,
}
function change( o ){
o.a +=10;
o.b += 100;
return o;
}
console.log(change(obj));
console.log(obj);
命名参数O 和 变量obj 存储的值相同 即指向 同一个对象的引用地址. 所以change函数改变了对象内部的值 打印变量obj 结果也会随之改变.
4. 没有重载
java中一个函数可以有两个定义, 只要签名(接收参数的类型和数量)不同就行. ECMAScript函数不能像java等传统编程这样重载. 因为ECMAScript函数的参数是由包含 零个或多个值的数组表示.没有函数签名,也就没有重载.
如果在ECMAScript中定义了两个同名的函数 后定义的会覆盖先定义的. 函数变量名被重写 保存第二个函数对象的引用地址.
5. 默认参数值
在ES5及以前, 实现默认参数的一种常用方式 就是在函数内部检测某个参数 是否等于 undefined .如果是undefined就 意味着没有传这个参数 那就给它赋一个值.
在函数内部 name = ( typeof name !=='undefined' ) ? name : 'Henry'; 如果参数不是undefined那么 就等于它自己 否则就是默认值 'Henry'.
ES6之后, 支持显式定义默认参数. 只要在函数定义中的 参数后面用 = 就可以为参数赋一个默认值.
funtion( name = 'henry' , age =18 ){ }
注意 : 使用默认参数时, arguments对象的值 只反映 函数调用时传给函数的参数, 不会受参数默认值的影响. 修改命名参数也不会影响arguments对象.它始终以调用函数时传入的值为准.( 不使用默认参数值时 ,arguments对象会受 命名参数值的影响 与之同步改变 )
函数的默认参数 只有在函数被调用时 才会求值 不会在函数定义时求值.
箭头函数 同样可以使用默认参数值, 只是参数的 ( ) 不能被省略了let hi = (name='Henry' ) =>{ ... }
默认参数作用域与暂时性死区
因为在求值默认参数时 可以定义对象,也可以动态调用函数, 如 let hi = ( name=getName() ) =>{ ... } 所以函数参数肯定是在某个作用域中求值的.
给多个参数 定义默认值 实际上跟使用let关键字顺序声明变量一样. 默认参数会按照定义他们的顺序 依次被初始化. 因此 后定义默认值的参数 可以引用先定义的参数. 看下面例子所示
function makeKing ( name = 'Henry', numerals = name ) {
return `King ${name} ${numerals}`;
}
console.log( makeKing() ); //King Henry Henry
//可以依照如下实例 想象一下这个过程
function makeKing () {
let name = 'Henry';
let numerals = name
return `King ${name} ${numerals}`;
}
console.log( makeKing() ); //King Henry Henry
参数初始化顺序 遵循"暂时性死区"规则, 即在参数定义前 ,不可以引用该参数.
function makeKing(name=numerals, numerals='VITT') {
return `King ${name} ${numerals}`;
}
如果调用上面的 makeMing函数 但是没有传第一参数 ,那么就会使用参数的默认值 numerals ,但是numerals变量还没有定义 所以这个时候会报错.
function makeKing(name = 'Henry', numerals = defaultNumeral) {
let defaultNumeral = 'VIII';
return `King ${name} ${numerals}`;
}
上面代码中 makeKing函数调用时不传第二个参数会报错
6. 参数扩展与收集
ES6新增了 扩展操作符, 使用它可以非常简洁地 操作和组合 集合数据. 扩展操作符 最有用的场景就是函数定义中的参数列表. 在这里它可以充分利用JS的弱类型及参数长度可变的特点.
扩展操作符 即可以用于调用函数时 传参, 也可以用于定义 函数参数.
6.1. 扩展参数
给函数传参时, 有可能不需要传一个数组, 而是分别传入数组的元素.
在ES6中 可以使用扩展操作符,极为简洁地实现这种操作. 对可迭代对象 应用扩展操作符, 并将其作为一个参数传入 可以将可迭代对象拆分 并将迭代返回的每个值单独传入.
let values = [1,2,3,4];
function getSum(){
let sum = 0;
for(let i=0 ;i<arguments.length; i++){
sum+= arguments[i];
}
return sum;
}
console.log( getSum(-1, ...values) ) //9
console.log( getSum( ...values,5) ) //15
console.log( getSum( ...values,5,...[2,3,4]) ) //24
对于函数中的 arguments而言,它并不知道 扩展操作符的存在,而是按照调用函数时 传入的参数接收每一个值.
arguments对象 只是消费扩展操作符的一种方式. 在普通函数(函数内部没有使用arguments对象)和箭头函数调用时 也可以将扩展操作符应用于命名参数. 这些函数在声明时也可以同时使用默认参数.
6.2. 收集参数
在函数定义时 可以使用扩展操作符. 那么在该函数调用时就可以 把不同长度的独立参数 组合为一个数组(例如下面例子中的values). 这有点类似arguments对象的构造机制 不过收集参数的结果会得到一个Array实例.
function getSum(...values){
//初始值总和为0 顺序累加values中的所有值
return values.reduce((x,y) => x+y, 0);
}
console.log(getSum(1,2,3) ) //6
收集参数的前面 如果还有命名参数,则只会收集其余的参数,如果没有其余的参数就会得到空数组. 因为收集参数的结果可以变,所以只把它作为最后一个参数, 否则就会报错 如下图所示
箭头函数虽然不支持 arguments对象 但是支持收集参数的定义方式,因此也可以实现 与使用arguments一样的逻辑.
使用收集参数 并不影响使用arguments对象 它依然反映调用时 传给函数的参数.
7. 函数声明与函数表达式
JS引擎在加载数据时, 对函数声明和函数表达式是区别对待的.
JS引擎 在任何代码执行之前 会先读取函数声明,并在执行上下文中 生成函数定义. 在执行代码时 JS引擎会先执行一遍扫描 把发现的函数声明提升到 源代码树的顶部. 因此函数声明可以出现在 调用该函数的代码之后. 这个过程叫做 函数声明提升.
而函数表达式 必须等到代码执行到它那一行 才会在执行上下文中生成函数定义.
除了函数什么时候 真正有定义这个区别之外 这两种语法是等价的.
8. 函数作为值
因为函数名 在ECMASript中就是变量,所以函数可以用在任何可以使用变量的地方. 不仅可以把函数作为参数传给另一个函数, 还可以在一个函数名中返回另一个函数.要注意的是,如果是访问函数而不是调用函数,那就必须不带括号.
把函数作为参数传给另一个函数:
function callSomeFunction(someFunction, someArgument) {
return someFunction(someArgument);
}
这个函数接收两个参数。第一个参数应该是一个函数,第二个参数应该是要传给这个函数的值。任
function add10(num) {
return num + 10;
}
let result1 = callSomeFunction(add10, 10);
console.log(result1); // 20
function getGreeting(name) {
return "Hello, " + name;
}
let result2 = callSomeFunction(getGreeting, "Nicholas");
console.log(result2); // "Hello, Nicholas"
在一个函数名中返回另一个函数:
假设有一个包含对象的数组,而我们想按照任意对象属性对数组进行排序。为此,可以定义一个 sort()方法需要的比较函数,它接收两个参数,即要比较的值。但这个比较函数还需要想办法确定根据哪个属性来排序。这个问题可以通过 定义一个根据属性名来创建比较函数的函数来解决。比如:
function createComparisonFunction( propertyName ) {
return function(object1, object2) {
let value1 = object1[propertyName];
let value2 = object2[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
let data = [
{name: "Zachary", age: 28},
{name: "Nicholas", age: 29}
];
data.sort(createComparisonFunction("name"));
console.log(data[0].name); // Nichola
arr.sort 方法的使用: sort方法会对数组进行 原位(in-place) 排序,更改元素的顺序。(译注:原位是指在此数组内,而非生成一个新数组。)
要使用我们自己的排序顺序,我们需要提供一个函数作为 arr.sort() 的参数。
if
(
a >
b)
return
1
;
// 表示第一个值比第二个值大
if
(
a ==
b)
return
0
;
// 表示两个值相等
if
(
a <
b)
return
-
1
;
// 表示第一个值比第二个值小
---------文章主要内容来自《JavaScript高级程序设计》-----------------------------------