JavaScript基础笔记-函数[上]

注意:本篇学习笔记基于原网站: JavaScript教程 - 廖雪峰的官方网站
笔记仅作学习留档使用

本篇目录

定义函数的两种方式
调用函数:arguments/rest参数/retun语句
全局作用域
名字空间
解构赋值(ES6)及实际使用场景
方法:apply和call/装饰鼹
高阶函数:map/reduce
扩展1:字符转数字的方法
扩展2:最好别在map里用parselnt

定义函数的两种方式+

//方法一:直接定义
function abs(x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
}
//方法二:赋值定义
let abs = function (x) {
    ...
};

在第二种定义函数的方式下,function (x) { ... }是一个匿名函数,这个函数赋值给了变量abs,所以abs就可以调用该函数。两种定义完全等价,注意赋值定义需要在函数体末尾加;表示赋值语句结束。

调用函数

JavaScript允许传入任意个参数而不影响调用,因此传入的参数与定义的参数不同不会报错:

  • 传多时,多余函数不会发生作用
  • 传少时,缺少的函数参数会被定义为undefined 加入函数操作
abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9
abs(); // x将收到undefined,计算结果为NaN

为了避免收到undefined,对参数进行传入确认:

function abs(x) {
    if (typeof x !== 'number') {
        throw 'Not a number';
    }
    ...
}

arguments

或者可以使用关键字arguments(实际上arguments就最常用于判断传入参数的个数),它用于函数内部,并且指向当前函数的调用者传入的所有参数。arguments类似Array但它不是

function foo(x) {
    console.log('x = ' + x); // 10
    for (let i=0; i<arguments.length; i++) {
		    // 10, 20, 30
        console.log('arg ' + i + ' = ' + arguments[i]); 
    }
}
foo(10, 20, 30);

//判断传入参数的个数用法
/*
foo(a[, b], c)
接收2~3个参数,b是可选参数,如果只传2个参数,b默认为null:
*/
function foo(a, b, c) {
    if (arguments.length === 2) {
        // 实际拿到的参数是a和b,c为undefined
        c = b; // 把b赋给c
        b = null; // b变为默认值
    }
    // ...
}

因为找的教程里讲的不是很清楚,去找了一下arguments在上面的情况下里面长这样:

arguments: {
  // 数字索引属性(存储实际参数值)
  0: 10,     // 第一个参数,对应形参 x
  1: 20,     // 第二个参数
  2: 30,     // 第三个参数
  
  // 核心属性
  length: 3,  // 参数的总个数
  
  // 内部属性(虽然不常用,但确实存在)
  callee: function foo(x) { ... },  // 指向当前正在执行的函数(即 foo 函数本身)
  
  // 其他内部属性和方法(在控制台中可以查看)
  __proto__: Object,           // 原型链指向
  Symbol(Symbol.iterator): ƒ values()  // 使得 arguments 可以被迭代(用于 for...of)
}

利用arguments,你可以获得调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值:

function abs() {
    if (arguments.length === 0) {
        return 0;
    }
    let x = arguments[0];
    return x >= 0 ? x : -x;
}

abs(); // 0

rest参数

ES6标准引入了rest参数,可以获取获取除了已定义参数之外的参数

//rest写在最后,前面用...标识
function foo(a, b, ...rest) {
    console.log('a = ' + a);
    console.log('b = ' + b);
    console.log(rest);
}

foo(1, 2, 3, 4, 5);
// 结果: a = 1; b = 2
// Array [ 3, 4, 5 ]  多余的参数以数组形式交给变量rest

foo(1);
// 结果:  a = 1; b = undefined
// Array []  传入的参数没填满rest参数会接收一个空数组

//用rest写的sum函数
function sum(...rest) {
    let s = 0;
    //console.log(rest.length);
    if (rest.length === 1){
        return rest[0];
    }
    if (rest.length > 1) {
        for (i = rest.length-1; i>=0; i--) {
            s = s + rest[i]
        }
    }
    return s;
}

return语句

JavaScript引擎有一个在行末自动添加分号的机制,会导致:

function foo() {
    return
        { name: 'foo' };
}

foo(); // undefined

//代码实际逻辑 ↓
function foo() {
    return; // 自动添加了分号,相当于return undefined;
        { name: 'foo' }; // 这行语句已经没法执行到了
}
 
//想写多行可以这样写:
function foo() {
    return { // 这里不会自动加分号,因为{表示语句尚未结束
        name: 'foo'
    };
}
  • 对于函数,如果没有return语句,函数执行完毕后也会返回结果,只是结果为undefined

全局作用域

函数外定义的变量具有全局作用域,且顶层函数(非嵌套内函数)的定义也被视为一个全局变量,每次直接调用的alert()函数其实也是window的一个变量。

JavaScript默认有一个全局对象window,全局作用域的变量会被绑到window的一个属性:

var course = 'Learn JavaScript';
console.log(course); // 'Learn JavaScript'
console.log(window.course); // 'Learn JavaScript'

function foo() {
    alert('foo');
}
foo(); // 直接调用foo()
window.foo(); // 通过window.foo()调用

JavaScript只有一个全局作用域。任何变量(函数也视为变量)如果没有在当前函数作用域中找到,就会继续往上查找直到全局作用域中也没有找到,则报ReferenceError错误。

名字空间

全局变量会绑定到window上,不同的JavaScript文件如果使用了相同的全局变量或相同名字的顶层函数,都会造成命名冲突

减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。
例如:

// 唯一的全局变量MYAPP:
let MYAPP = {};

// 其他变量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;

// 其他函数:
MYAPP.foo = function () {
    return 'foo';
};

把自己的代码全部放入唯一的名字空间MYAPP中,会大大减少全局变量冲突的可能。

解构赋值(ES6)

对数组元素进行解构赋值时,多个变量要用[...]括起来。

let [x, y, z] = ['hello', 'JavaScript', 'ES6'];
// x, y, z分别被赋值为数组对应元素:
console.log(`x = ${x}, y = ${y}, z = ${z}`);
//x = hello, y = JavaScript, z = ES6

//数组本身有嵌套
let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];
//x = hello, y = JavaScript, z = ES6

// 忽略前两个元素,只对z赋值第三个元素
let [, , z] = ['hello', 'JavaScript', 'ES6']; 
z; // 'ES6'

对对象取数/解构赋值

let person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school'
    address: {
        city: 'Beijing',
        street: 'No.1 Road',
        zipcode: '100001'
    }
};
let {name, age, passport} = person;

// name, age, passport分别被赋值为对应属性:
console.log(`name = ${name}, age = ${age}, passport = ${passport}`);

//对嵌套的对象属性进行赋值
let {name, address: {city, zip}} = person;
name; // '小明'
city; // 'Beijing'
zip; // undefined, 因为属性名是zipcode而不是zip

如果对应的属性不存在,变量将被赋值为undefined

// address是嵌套的address对象的属性
address; // Uncaught ReferenceError: address is not defined

// 如果要使用的变量名和属性名不一致
// 把passport属性赋值给变量id:
let {name, passport:id} = person;
id; // 'G-12345678'
passport; // Uncaught ReferenceError: passport is not defined

可以使用默认值,这样就避免不存在的属性返回undefined

// 如果person对象没有single属性,默认赋值为true:
let {name, single=true} = person;
name; // '小明'
single; // true

已经被声明变量再次赋值:

// 声明变量:
let x, y;
// 解构赋值:要小括号
({x, y} = { name: '小明', x: 100, y: 200});
//没小括号会报错
// 语法错误: Uncaught SyntaxError: Unexpected token =
{x, y} = { name: '小明', x: 100, y: 200};

实际使用场景

// 交换变量值
let x=1, y=2;
[x, y] = [y, x]

// 快速获取当前页面的域名和路径
let {hostname:domain, pathname:path} = location;

// 快速创建一个Date对象
function buildDate({year, month, day, hour=0, minute=0, second=0}) {
    return new Date(`${year}-${month}-${day} ${hour}:${minute}:${second}`);
}
buildDate({ year: 2017, month: 1, day: 1 });
// Sun Jan 01 2017 00:00:00 GMT+0800 (CST)
buildDate({ year: 2017, month: 1, day: 1, hour: 20, minute: 15 });
// Sun Jan 01 2017 20:15:00 GMT+0800 (CST)

方法

let xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        let y = new Date().getFullYear();
        return y - this.birth;
    }
};

xiaoming.age; // function xiaoming.age()
xiaoming.age(); // 今年调用是25,明年调用就变成26了

this

绑定到对象上的函数称为方法,内部有个特殊变量this始终指向当前对象,也就是xiaoming这个变量。所以,this.birth可以拿到xiaomingbirth属性。

function getAge() {
    let y = new Date().getFullYear();
    return y - this.birth;
    //此时函数的this指向全局对象,也就是window
}

let xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge
};

xiaoming.age(); // 25, 正常结果
getAge(); // 调用出错所以NaN

//从外部重赋函数也不行
let fn = xiaoming.age; // 先拿到xiaoming的age函数
fn(); // NaN

注意:在strict模式下让函数的this指向undefined

例如这里报错是因为this指针只在age方法的函数内指向xiaoming,在函数内部定义的函数,this指向undefined(非strict模式下指向全局对象window

'use strict';
let xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        function getAgeFromBirth() {
            let y = new Date().getFullYear();
            return y - this.birth;
        }
        return getAgeFromBirth();
    }
};
xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined

修复方案:用that变量捕获thislet that = this;

'use strict';
let xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        let that = this; // 在方法内部一开始就捕获this
        
        function getAgeFromBirth() {
            let y = new Date().getFullYear();
            // 用that而不是this
            return y - that.birth; 
        }
        return getAgeFromBirth();
    }
};
xiaoming.age(); // 25

apply和call

apply可以指定函数的this指向哪个对象,接收两个参数,第一个参数就是需要绑定的this变量,第二个参数是Array,表示函数本身的参数。

function getAge() {
    let y = new Date().getFullYear();
    return y - this.birth;
}

let xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge
};

xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空

或者用call()apply()把参数打包成Array再传入,call()把参数按顺序传入。

//使用区别
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5

对普通函数调用,我们通常把this绑定为null

装饰器

利用apply()动态改变函数的行为,JavaScript的所有对象都是动态的,内置的函数也可以重新指向新的函数。例如,统计一下代码一共调用了多少次parseInt()

'use strict';

let count = 0;
let oldParseInt = parseInt; // 保存原函数

//将原装函数parseInt换成我们自己设计的匿名函数
window.parseInt = function () {
    count += 1; //增加计数步骤
    return oldParseInt.apply(null, arguments); // 调用原函数
    //arguments记录所有传入的函数
};

// 测试:
parseInt('10');
parseInt('20');
parseInt('30');
console.log('count = ' + count); // 3

高阶函数

接收另一个函数作为参数的函数

function add(x, y, f) {
    return f(x) + f(y);
}

add(-5, 6, Math.abs)
/*
x = -5;
y = 6;
f = Math.abs;
f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11;
return 11;
*/

map

map()方法定义在JavaScript的Array中,调用Arraymap()方法,传入自己的函数会得到一个新的Array作为结果。注意map()传入的参数是函数对象本身。

function pow(x) {
    return x * x;
}

let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
// 传入函数对象pow
let results = arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
console.log(results);

// 还可以这样
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']

reduce

原博这么说:

  • Array的reduce()把一个函数作用在这个Array[x1, x2, x3...]上,这个函数必须接收两个参数,reduce()把结果继续和序列的下一个元素做累积计算。

大概意思是,reduce()函数起到一类似于“累加器”一样的累计处理作用。假设Array = [a0, a1, a2, a3]结合代码解释是这样:

arr = [a0, a1, a2, a3]
//定义函数add(x, y),函数接受x和y两个参数
//(只能接受两个参数的函数reduce才能用)
function add(x, y) {
    return x + y;
}
//reduce使用函数add
let result = arr.reduce(add)
/*
内部过程:
取出arr[0] 与 arr[1]即 a0 与 a1
令x = a0, y = a1;add(x, y) = a1+a2
方便起见我们设r1 = a1 + a2
取出 arr[2] = a2
令x = r1, y = a2;add(x, y) = r1+a2 = a0+a1+a2
....(重复上面过程)
直到最后arr[4] 取出来没东西了,reduce结束
result=a0 + a1 + a2 + a3
*/

如果Array只有一个元素的场合:

let arr = [123];
// 提供一个额外的初始数据0
// 顺便这里是在使用reduce时直接创建简单的匿名函数f(x, y)
//map也可以这样写
arr.reduce(function (x, y) {return x + y;}, 0);
//这样也可以(map同理arr.map(ch => ch - 0))
arr.reduce((x, y) => x + y, 0);

扩展1:字符转数字的方法

//1. 使用parseInt、parseFloat和Number函数
parseInt("123.45")//123,保留为整数
parseFloat("123.45")//123.45
Number("123.45")//123.45严格转换
//顺便如果用parseInt有一个好处,能忽略一些结尾非数字元素
parseInt("123abc")//123,如果用number这里会输出NaN

//2. ASCII 码差值法
let char = '7';
let num = char.charCodeAt(0) - '0'.charCodeAt(0);//7
//或者
let num = '5'.charCodeAt(0) - 48;  // 因为 '0' 的 ASCII 码是 48

//3. 减法隐式转换
// JavaScript 在做减法时会尝试将操作数转为数字
let char = '3';
let num = char - 0;  // 字符减 0 会被转为数字

//4. 位运算
let char = '9';
let num1 = str | 0; 
let num2 = ~~char;  // 位运算会强制转为整数

//5. 取整
let str = "123.7";
Math.floor(+str); // 123(向下取整)
Math.round(+str); // 124(四舍五入)

//6.一元加号运算符
let str = "123";
let num = +str; // 123

扩展2:最好别在map里用parseInt

直接在map里用parseInt很可能导致输出NaN

const arr = ['1', '2', '3', '4'];
// 你期望的结果
arr.map(parseInt);  // 返回 [1, 2, 3, 4]?
// 实际结果
console.log(arr.map(parseInt));  // 实际是 [1, NaN, NaN, NaN]

原因是map会给回调函数传递三个参数:当前元素、当前索引、整个数组。而parseInt接受两个参数:要解析的字符串、基数(几进制的意思)(要求为2-36 之间的整数,或者特殊情况用0)。导致parseInt接受map时会只接受map给它的前两个参数,会运行成下面这个样子:

//第一次调用:
parseInt('1', 0, ['1', '2', '3', '4'])
// parseInt('1', 0) -> 1 (基数为0时,如果字符串不以"0x"开头,则基数为10)

// 第二次调用:
parseInt('2', 1, ['1', '2', '3', '4'])
// parseInt('2', 1) -> NaN (基数为1是无效的,必须在2-36之间)

// 第三次调用:
parseInt('3', 2, ['1', '2', '3', '4'])
// parseInt('3', 2) -> NaN (二进制中没有3)

// 第四次调用:
parseInt('4', 3, ['1', '2', '3', '4'])
// parseInt('4', 3) -> NaN (三进制中没有4)
posted @ 2025-12-15 09:12  qiqimk  阅读(3)  评论(0)    收藏  举报