JavaScript-函数

函数基础

定义

1
2
3
4
5
6
7
function abs(x) {
if (x >= 0) {
return x;
} else {
return -x;
}
}
  • funciton: 定义函数的关键字
  • abs: 函数名
  • (x): 函数参数
  • {...}: 函数体

还有一种定义方式

1
2
3
4
5
6
7
let abs = function (x) {
if (x > 0) {
return x;
} else {
return -x;
}
}; // 注意这种方式末尾需要加;
  • function (x) {...}: 是一个匿名函数
  • abs: 这个匿名函数赋值给了变量abs,所以通过abs就能调用该函数

调用

1
2
3
4
5
abs(-9); // 返回9
// JS允许传入任意数量的参数,所以不论传多了还是传少了都不影响调用
abs(-9, 'haha', 'hehe', null); // 返回9
abs(); //返回NaN
// 此时abs的参数x将收到undefined,计算结果就是NaN

为了避免收到undefined,可以对参数检查

1
2
3
4
5
6
7
8
9
10
function abs(x) {
if (typeof x !== 'number') {
throw 'Not a number';
}
if (x > 0) {
return x;
} else {
return -x;
}
}

arguments

arguments是js的一个关键字,只能在函数内部起作用,永远指向当前函数的调用者传入的所有参数.类似array但并不是array.

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

利用arguments就可以获取传入的所有参数,所以即使不定义参数,也能获取传入的参数.

1
2
3
4
5
6
7
function abs() {
if (arguments.length === 0) {
return 0;
}
let x = arguments[0];
return x >= 0 ? x : -x; // 三元运算符/条件运算符,很多语言都有类似的写法
}

实际上arguments最常用于判断传入参数的个数

1
2
3
4
5
6
7
8
9
10
// 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变为默认值
}
// ...
}

rest

假如你的函数只定义了2个参数,但我们要使用除了前两个已定义参数外的所有参数,此时是使用arguments就会显得很麻烦.ES6后引入了...rest关键字来获取不定参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 ]
foo(1)
// 结果:
// a = 1
// b = undefined
// Array []
  • ...rest只能写在最后
  • 如果传入的参数连已定义的参数都没有填满,rest会接收一个空数组(非undefined)

return不要分行

因为JS引擎会自动加;,如果return分行写的话容易变成这样

1
2
3
4
5
function a() {
...
return; // 引擎自动加上;
{ name: 'foo' };
}

那么返回值就会出错.正确的写法是:

1
2
3
4
5
6
function a() {
...
return { // { 表示语句还没完结,不会自动加;
name: 'foo'
};
}

变量作用域

函数作用域

  • 如果一个变量在函数体内申明,该变量的作用域为整个函数体,函数体外不可引用该变量

  • 不同函数内部的同名变量互相独立,互不影响

  • JS的函数可以嵌套,内部函数可以访问外部函数定义的变量,反之则不行

  • JS的函数在查找变量时从自身函数定义开始,如果内部函数定义了与外部函数重名的变量,则以内部变量的定义为准

  • JS会扫描整个函数体的语句,把所有申明的变量提升到函数顶部

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function foo() {
    var x = 'Hello, ' + y; // 这里不会报错,因为JS会自动提升变量y的申明
    console.log(x);
    var y = 'Bob'; // 但是它只提升申明,不提升赋值,所以最终输出会是undefined
    }
    foo();
    /*
    输出如下:
    Hello, undefined.
    */

因此,我们在函数内部定义变量,必须严格遵守在函数内部首先申明所有变量这一原则.最常见的做法是用一个var申明所有用到的变量.

1
2
3
4
5
6
7
8
function foo() {
var
x = 1, // x初始化为1
y = x + 1, // y初始化为2
z, i; // z,i初始化为undefined
//其他语句
...
}

ES6后,建议使用let代替var.

全局作用域

不在任何函数内定义的变量就具有全局作用域.JS默认有一个全局对象window,全局作用域的变量都会被绑定成window的一个属性.

1
2
3
4
var course = 'Learn JavaScript';
console.log(course);
console.log(window.course);
// 因此上面两个访问方式等价

同理,函数的另一种定义方式,把匿名函数赋值给变量,这个变量实际上也是一个全局变量.因此,顶层函数的定义也被视为一个全局变量,绑定到window的一个属性.

1
2
3
4
5
6
function foo() {
alert('foo');
}

foo();
window.foo(); //两种调用同价

其实不难想到,每次调用的alert()函数其实也是window的一个变量

1
2
3
4
5
6
7
8
9
10
11
window.alert('调用window.alert()');
// 把alert保存到另一个变量:
let old_alert = window.alert;
// 给alert赋一个新函数:
window.alert = function () {}

alert('无法用alert()显示了!');

// 恢复alert:
window.alert = old_alert;
alert('又可以用alert()了!');

这说明JS只有一个全局作用域,任何变量(函数也被视为变量),如果在当前函数作用域没有找到,就会继续往上查找,最后如果在全局作用域中也没有,就会报ReferenceError.

名字空间

所有全局变量都会绑定到window上,不同的JS文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,就会造成冲突,而且难以发现.一个解决办法就是,把自己的所有变量和函数全部绑定到一个全局变量中.这个全局变量也被称为名字空间.

1
2
3
4
5
6
7
8
9
10
11
// 唯一全局变量
let MYAPP = {};

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

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

很多著名的JS库都是这样做: JQuery, YUI, underscore等

块级作用域

JS的变量作用域实际上是函数内部,我们在for循环等语句块中无法定义具有局部作用域的变量.

因此,ES6引入了let关键字,可以用来申明一个块级作用域的变量.

1
2
3
4
5
6
7
function foo() {
let sum = 0;
for (let i=0; i<100; i++) {
sum += i; // 块内的可以调用块外的
}
i += 1; // 因为i是for快内的变量,使用let定义,所以块外无法调用,会报错SynTaxError
}

常量

ES6前,通常用一个全大写的变量来表示”这是一个常量,不要修改它的值”

1
let PI = 3.14;

ES6后,引入const来定义常量,与let一样都具有块级作用域

1
2
3
const PI = 3.14;
PI = 3; // 某些浏览器不报错,但无效果
PI; // 依旧是3.14

const, let, var

首先const用来定义常量,它不可变,具有块级作用域.

需要特别区分的是letvar

区别点letvar
作用域块级函数
变量提升不会提升(先申明再使用)提升申明
全局对象属性不会加到window的属性会加到window的属性
重复声明不允许,会报错允许,值可变

总结:

现在JS开发中,推荐使用letconst来替代var.这样可以避免一些常见的作用域相关问题,使代码更可预测和更容易维护.

某些特殊情况需要用到var:

  1. 老旧浏览器兼容性问题
  2. 特殊的跨作用域访问场景
  3. 特定的闭包使用场景

解构赋值

一次过赋值多个变量

1
let [x, y, z] = ['hello', 'JavaScript', 'ES6'];

嵌套的话注意位置保持一致

1
2
3
4
let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];
x; // 'hello'
y; // 'JavaScript'
z; // 'ES6'

忽略元素赋值

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

从一个对象中提取多个属性

1
2
3
4
5
6
7
8
let person = {
name: '小明',
age: 20,
gender: 'male',
passport: 'G-12345678',
school: 'No.4 middle school'
};
let {name, age, passport} = person

同样,对于嵌套的对象属性也可以

1
2
3
4
5
6
7
8
9
10
11
12
13
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, address: {city, zip}} = person;

使用场景

解构赋值可以大大简化代码.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 交换两个变量的值,不需要临时变量
let x=1, y=2;
[x, y] = [y, x];
// 快速获取当前页面的域名和路径
let {hostname:domain, pathname:path} = location; // location是浏览器提供的全局对象之一

// 传一个对象给函数,通过解构,将对象属性直接赋值给参数
function printUserInfo({ name, age }) {
console.log(`${name} is ${age} years old`);
}
const user = {
name: 'Tom',
age: 20,
city: 'Beijing'
};
printUserInfo(user); // "Tom is 20 years old"

方法

this

在一个对象中绑定一个函数,该函数即为该对象的方法.

1
2
3
4
5
6
7
8
9
10
11
let xiaoming = {
name: '小明',
birth: 1990,
age: function () {
let y = new Date().getFullYear();
return y - this.birth
}
}

xiaoming.age; // 返回函数自身 f () {...}
xiaoming.age(); // 今年调用是25,明年调用就变成26了

this类似python的self,是一个特殊变量,始终指向当前对象,也就是xiaoming.然而this有一些地方需要注意:

  • 只有通过obj.function()方式调用,this才能正确指向对象

  • 其他调用会直接指向全局对象window

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function getAge() {
    let y = new Date().getFullYear();
    return y - this.birth;
    }

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

    xiaoming.age(); // 25, 正常结果
    getAge(); // 直接调用,this指向window,自然没有birth这个属性,返回NaN

    // 这样写也是不行的
    let fn = xiaoming.age; // 先拿到xiaoming的age函数
    fn(); // NaN
  • strict模式下,错误的调用会让this指向undefined

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    'use strict';

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

    let fn = xiaoming.age;
    fn(); // Uncaught TypeError: Cannot read property 'birth' of undefined
  • 内嵌函数内的调用也会指向全局对象,可以先用that关键字捕获

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    'use strict';

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

    xiaoming.age(); // 25

apply

在独立函数调用中的this:

  • 使用strict,this指向undefined
  • 不用strict,this指向window的属性

如果要控制this的指向,规定它指向哪个对象,可以使用函数本身的apply方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
function getAge() {
let y = new Date.getFullYear();
return y - this.birth;
}

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

xiaoming.age(); // 正常调用
getAge.apply(xiaoming, []); // 第一个参数是需要绑定的this对象,第二个参数array,表示需要传给函数的参数

另一个与apply()类似的方法是call(),区别在于:

  • apply()把参数打包成Array再传入
  • call()把参数按顺序传入
1
2
Math.max.apply(null, [3, 5, 4]);
Math.max.call(null, 3, 5, 4);

对于普通函数的调用,我们通常把this绑定到null.

普通函数是指不依赖任何对象上下文的函数,特别像Math.max()这样的工具函数.与之相对的就是对象的方法.

普通函数根本不会使用this,传入null只是一种约定俗成的做法,表明我们不关心this的值.

装饰器

1
2
3
4
5
let oldParseInt = parseInt; // parseInt是JS的一个全局函数,用于将字符串转换为整数,这里先把它保存起来
window.parseInt = function () {
count += 1; // 添加计数器
return oldParseInt.apply(null, arguments); // 调用原始函数
}

其实这里就相当于重写parseInt,添加计数器功能,然后通过apply,保留原函数的功能.这样每次调用该函数时都会自动计数.

这就是JS典型的装饰器(Decorator)模式.

这种模式一般用于:

  • 函数调用统计
  • 性能监控
  • 日志记录
  • 调试

高阶函数

higher-order function: 一个函数以另一个函数作为参数,就称为高阶函数.

JS 的函数都指向某个变量,变量可以指向函数,函数的参数可以接收变量,所以函数可以作为另一个函数的参数.

1
2
3
4
5
6
// 一个简单的高阶函数例子
function add(x, y, f) {
return f(x) + f(y);
}

add(-5, 6, Math.abs)

map

比如我们有一个函数f(x)=x*x,要对一个数组上的所有元素都使用这个函数,此时就可以使用map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
            f(x) = x * x



┌───┬───┬───┬───┼───┬───┬───┬───┐
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼

[ 1 2 3 4 5 6 7 8 9 ]

│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼

[ 1 4 9 16 25 36 49 64 81 ]

JS的map()定义在Array中,所以我们调用Arraymap(),传入自己的函数,就得到一个新得Array返回.

1
2
3
4
5
6
7
function pow(x) {
return x * x;
}

let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
let results = arr.map(pow);
console.log(results)

当然,我们也可以写个循环实现上面的目的.但是map()作为高阶函数,事实上它把运算规则抽象了,所以我们可以根据传入的函数,实现各种复杂的操作,代码还能保持简单明了.

1
2
3
// 比如把Array所有数字转为字符串,只需一行代码
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']

reduce

reduce同样也是作用在array的每个元素上,但是它是累积的.

比如通过reduce把函数f(x)作用在[x1, x2, x3, x4]上,其效果就是

1
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)
1
2
3
4
let arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x + y;
}); //25

如果数组元素只有一个,就要提供一个额外的初始参数

1
2
3
4
let arr = [123];
arr.reduce(function (x, y) {
return x + y;
}, 0); // 额外的初始参数0

[1, 3, 5, 7, 9]变成一个整数

1
2
3
4
let arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x * 10 + y;
});

不使用parseInt(),利用mapreduce,把'13579'转为数字.

1
2
3
4
5
6
7
8
9
10
11
function string2int(s) {
let arr = s.split(''); //空字符串作为分隔符,逐一字符提取成array
let a = arr.map(function (x) {
return x - '0'; // - '0'是JS的一个常用技巧,当'0'参与运算,JS会自动把它转为Number,包括最后结果的值也会是Number
// return + x; 这叫一元加号运算符,也是可以把字符串转为数字的一个技巧
});
let result = a.reduce(function (x, y) {
return x * 10 + y;
});
return result; // 13579
}

filter

filter用于把array的某些元素过滤掉,返回剩下的.

filter把传入的函数依次作用到array每个元素,根据返回值true/false来决定保留还是丢弃.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 例如踢出偶数
let arr = [1, 2, 4, 5, 6, 9, 10, 15];
let r = arr.filter(function (x) {
return x % 2 !== 0;
});
r; // [1, 5, 9, 15]

// 删除空字符串
let arr = ['A', '', 'B', null, undefined, 'C', ' '];
let r = arr.filter(function (s) {
return s && s.trim();
});
r; // ['A', 'B', 'C']

回调函数

callback function是一个编程概念.作为参数传递给另一个函数的函数,则称为回调函数.当某个事件发生或特定条件满足时,这个被传递的函数就会被”调用回来”执行.

filter接收的回调函数其实可以由多个参数:

  1. 第一个: 表示array的某个元素
  2. 第二个: 表示元素的位置
  3. 第三个: 数组本身

类似的还有reducemap

1
2
3
4
5
6
let arr = ['A', 'B', 'C'];
let r = arr.filter(function (element, index, self) {
console.log(element); // 依次打印'A', 'B', 'C'
console.log(index); // 依次打印0, 1, 2
console.log(self); // self就是变量arr
})

filter去除重复元素

1
2
3
4
5
6
7
let 
r,
arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];

r = arr.filter(function (element, index, self) {
return self.indexOf(element) === index; // indexOf返回array中element的第一次出现的索引,如果当前索引index不等于indexOf返回的,说明该元素不是第一次出现,需要过滤掉
})

sort

排序的核心是比较两个元素的大小,但是如果元素是字符串,在数学上则无法比较.因此比较的过程必须抽象出来.通常规定:

  • x<y返回-1
  • x=y返回0
  • x>y返回1

排序算法不关心具体的比较过程,只根据结果排序.

JS 的array有个sort()方法,然而有点坑爹.

1
2
3
4
5
6
7
8
// 看上去正常的结果:
['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft'];

// apple排在了最后:
['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple']

// 无法理解的结果:
[10, 20, 1, 2].sort(); // [1, 10, 2, 20]

第二个排序apple排在最后,那时因为小写字母a的ASCII码在大写字母之后.

第三个则是因为,sort()默认把所有元素先转换成String再排序,结果'10'排在'2'前面,因为'1'的ASCII码比'2'小.

还好的是sort()是一个高阶函数,我们可以自己实现比较函数传进去比较.

1
2
3
4
5
6
7
8
9
10
11
let arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
if (x < y) {
return -1;
}
if (x > y) {
return 1;
}
return 0;
})
console.log(arr); // [1, 2, 10, 20]

如果要实现倒序排序,可以这样

1
2
3
4
let arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
return y - x;
}); // [20, 10, 2, 1]

y-x:

  • 如果x<y,则返回正数
  • 如果x>y,则返回负数
  • 如果x=y,则返回0

天才

默认情况下对字符串排序是通过比较ASCII码来实现,那如果想忽略大小写来排序呢?也不难,把元素全部转为大写/小写再比较就行了.

1
2
3
4
5
6
7
8
9
10
11
12
let arr = ['Google', 'apple', 'Microsoft'];
arr.sort(function (s1, s2) {
x1 = s1.toUpperCase();
x2 = s2.toUpperCase();
if (x1 < x2) {
return -1;
}
if (x1 > x2) {
return 1;
}
return 0;
});

最后从上面不难看出,sort()是会直接修改原array的.这个要注意一下.

1
2
3
4
5
let a1 = ['A', 'B', 'C'];
let a2 = a1.sort();
a1; // ['A', 'B', 'C']
a2; // ['A', 'B', 'C']
a1 === a2; // true

array的其他高阶函数

every

用于判断数组的所有元素是否满足测试条件

1
2
3
4
5
6
7
8
let arr = ['Apple', 'pear', 'orange'];
console.log(arr.every(function (s) {
return s.length > 0;
})); // true, 因为所有元素都满足s.length > 0

console.log(arr.every(function (s) {
return s.toLowerCase() === s;
})); // false,因为不是每个元素都是小写

find

用于查找符合条件的第一个元素,找到就返回该元素,否则就返回undefined.

1
2
3
4
5
6
7
8
let arr = ['Apple', 'pear', 'orange'];
console.log(arr.find(function (s) {
return s.toLowerCase() === s;
})); // 'pear', 因为pear全是小写

console.log(arr.find(function (s) {
return s.toUpperCase() === s;
})); // 返回undefined,因为没有全是大写的元素

findIndex

find类似,也是找符合条件的第一个元素,但是它返回的是index,没有找到则返回-1

1
2
3
4
5
6
7
8
let arr = ['Apple', 'pear', 'orange'];
console.log(arr.findIndex(function (s) {
return s.toLowerCase() === s;
})); // 1, 因为'pear'的索引是1

console.log(arr.findIndex(function (s) {
return s.toUpperCase() === s;
})); // -1

forEach

这个跟map()类似,也是把每个元素依次作用与传入的函数,但不会返回新数组.

forEach常用于遍历数组,传入的函数不需要返回值.

1
2
let arr = ['Apple', 'pear', 'orange'];
arr.forEach(x=>console.log(x)); // 依次打印每个元素

返回函数

高阶函数除了可以接收函数作为参数,还可以返回函数.

比如我们要实现一个对array的求和函数,但是我们不需要立即求和,而是在后面的代码中根据需要再计算:

1
2
3
4
5
6
7
8
9
10
11
12
13
function lazy_sum(arr) {
let sum = function () {
return arr.reduce(function (x, y) {
return x + y;
});
}
return sum; // 返回函数
}

let f = lazy_sum([1, 2, 3,, 4, 5]); // 这里返回的就是一个函数
f(); // 15
let f2 = lazy_sum([1, 2, 3,, 4, 5]);
f === f2; // false; 每次返回都会是一个全新的函数

闭包

闭包是JS的一个重要特性,指的是函数能够记住并访问它的词法作用域,即使当这个函数在其原始作用域之外执行时也能正常工作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createCounter() {
let count = 0; // 私有变量
return { // 返回一个函数填充的array
increment: function() {
count++;
return count;
},
getCount: function() {
return count;
}
};
}

const counter = createCounter();
console.log(counter.getCount()); // 0
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2

上面的例子说明,即使变量是定义在函数内部的,但是通过return返回的函数依旧可以正常访问/修改私有变量.因为返回函数形成了闭包.

但有一点要注意,返回的函数不会在返回时执行,而是在实际调用的才会执行.

闭包陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function count() {
let arr = [];
for (var i=1; i<=3; i++) { // var是函数作用域申明,所有推入数组得函数都会引用同一个i
arr.push(function (){
return i * i;
});
}
return arr;
}

let result = count();
let [f1, f2, f3] = result;
// 再加上闭包,函数在被调用时才会执行,所以当执行返回的函数时,i已经变成4
f1(); // 16
f2(); // 16
f3(); // 16

要避免这种陷阱,你可以:

  1. 使用let代替var,let作用域决定了每次循环都会绑定新的i.(recommend)

  2. 创建立即执行函数,这是JS的一个语法,用于创建一个匿名函数并立刻执行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function count() {
    let arr = [];
    for (var i=1; i<=3; i++) {
    arr.push((function (n) {
    return function () {
    return n * n;
    }
    })(i));
    }
    return arr;
    }

    let [f1, f2, f3] = count();

    f1(); // 1
    f2(); // 4
    f3(); // 9

    这里再创建了一个函数,用该函数的参数绑定循环变量的当前值,无论该循环后续如何更改,已绑定到函数参数的值不变.

    可以看出,里面嵌套很复杂,最好还是不要用这种,这里更多是为了引出创建一个匿名函数并立刻执行的语法:

    1
    2
    3
    4
    5
    6
    7
    8
    // 理论上讲,创建一个匿名函数并立刻执行可以这么写
    function (x) {return x * x} (3);
    // 但是JS语法解析的问题,需要用括号把函数括起来,不然会报SyntaxError
    (function (x) {return x * x}) (3);
    // 通常会把它拆开写,好看点
    (function (x) {
    return x * x;
    }) (3);
  3. 不要在返回函数中引用循环变量.(recommend)

私有变量

JS的闭包除了返回一个函数,延迟执行外,还有一个最强大的功能,就是在对象内部封装一个私有变量.

想java和c++,可以用private,但JS没有class只有函数,需要借助闭包,封装私有变量.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function create_counter(initial) {
let x = initial || 0; // 如果initial有值就使用initial,否则为0, 一种设置默认值的简写方式
return {
inc: function () {
x += 1;
return x;
}
}
}

//用起来是这样的
let c1 = create_counter();
c1.inc(); // 1
c1.inc(); // 2
c1.inc(); // 3

let c2 = create_counter(10);
c2.inc(); // 11
c2.inc(); // 12
c2.inc(); // 13

在返回的对象中,实现了一个闭包,闭包携带了局部变量x,并且这个x无法被访问.换句话说,闭包就是携带状态的函数,并且它的状态可以完全对外隐藏起来.

创建新函数

举个例子: 闭包还可以把多参数的函数变成单参数的函数.比如要计算x^y可以用Math.pow(x, y)函数,不过考虑到经常计算x^2或x^3,可以利用闭包创建新的函数pow2pow3.

1
2
3
4
5
6
7
8
9
10
11
function make_pow(n) {
return function (x) {
return Math.pow(x, n)
}
}
// 创建两个新函数
let pow2 = make_pow(2);
let pow3 = make_pow(3);

console.log(pow2(5)); // 25
console.log(pow3(7)); // 343

这种做法也被称为函数工厂,这种函数也被称为工厂函数.

箭头函数

ES6新增一种新的函数: 箭头函数,因为它的定义就是一个箭头=>

1
2
3
4
5
x => x * x
// 上面的箭头函数相当于
function (x) {
return x * x;
}

箭头函数相当于匿名函数,并且简化了函数定义.它有两种定义格式:

  1. 像上面的,只包含一个表达式,{...}return都省略

  2. 包含多种语句的,不能省略{...}return

    1
    2
    3
    4
    5
    6
    7
    x => {
    if (x > 0) {
    return x * x;
    } else {
    return - x * x;
    }
    }

如果参数不是只有一个,就要用()括起来

1
2
3
4
5
6
7
8
9
10
11
12
// 两个参数
(x, y) => x * x + y* y;
// 没有参数
() => 3.14;
// 可变参数
(x, y, ...rest) => {
let i, sum = x + y;
for (i=0; i<rest.length; i++) {
sum += resy[i];
}
return sum;
}

如果返回一个对象

1
2
x => {foo: x} // 这样写是错误的SynctaxError,因为和函数体{...}有语法冲突
x => ({foo: x}) // 加个括号就可以了

this修复

箭头函数没有自己的 this 绑定。箭头函数会捕获其被定义时的上下文的 this 值,而且这个绑定是永久的,不能被改变。

词法作用域(Lexical scope):

是指变量的作用域在代码编写时(词法分析时)就已经确定,而不是在运行时确定.简单来说,就是变量的访问权限由变量定义的位置决定.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 普通函数
let obj = {
bitrh: 1990,
getAge: function () {
let b = this.birth;
let fn = function () {
return new Date().getFullYear() - this.birth; // this指向window或undefined
};
return fn();
}
}

// 箭头函数
let obj = {
bitrh: 1990,
getAge: function () {
let b = this.birth;
let fn = () => new Date().getFullYear() - this.birth; // 使用了箭头函数,this指向外部域,也就是getAge函数的域
return fn();
}
}

再来两个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 例子一
const obj = {
name: "张三",
// 普通函数
sayHiNormal: function() {
console.log(`Hi, ${this.name}`);
},
// 箭头函数没有自己的this,只能捕获定义时所在上下文的this值
sayHiArrow: () => {
console.log(`Hi, ${this.name}`);
}
};

obj.sayHiNormal(); // Hi, 张三
obj.sayHiArrow(); // Hi, undefined (因为箭头函数的this指向外部作用域)

// 例子二
let obj = {
birth: 1990,
getAge: function () {
let b = this.birth; // 这里的 this 指向 obj
let fn = () => new Date().getFullYear() - this.birth;
// 箭头函数在 getAge 函数内部定义
// 所以它继承了 getAge 的 this,即 obj
return fn();
}
};

第一个例子的箭头函数直接在对象字面量中定义,所以继承了全局作用域的this

对象字面量: 直接使用{}创建对象的语法

第二个例子的箭头函数在普通函数getAge内定义,继承外部作用域,也就是getAgethis

1
2
3
4
5
6
7
8
9
10
// 再来一个例子
let obj = {
birth: 1990,
getAge: function (year) {
let b = this.birth; // 1990
let fn = (y) => y - this.birth; // 由于箭头函数的this是上下文决定的,所以这里的this和getAge函数保持一致,都是被调用时的对象.
return fn.call({birth:2000}, year); // 此时用call去调用fn,第一个参数要传入一个调用对象,但是由于箭头函数的this不可改变,所以依旧是obj
}
};
obj.getAge(2015); // 25

标签函数

对于模板字符串,除了方便引用变量构造字符串外,还有一个更强大的功能,就是使用标签函数(tag function)

1
2
3
4
// 模板字符串
let name = '小明';
let age = 20;
let s = `你好, ${name}, 你今年${age}岁了!`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const email = 'test@example.com';
const password = 'hello123';

function sql(string, ...exps) {
console.log(`SQL: ${string.join('?')}`);
console.log(`SQL paramenters: ${JSON.stringify(exps)}`);
return {
name: '小明',
age: 20
};
}

const result = sql`SELECT * FROM users WHERE email=${email} AND password=${password}`; // 标签函数
console.log(JSON.stringify(result));

模板字符串以特定标签函数名开头,就是标签函数的调用方式.

上面就是调用标签函数sql().

标签函数接收两个参数:

  • 将模板字符串的字符串部分,转换成数组,赋值给第一个参数string,上面的例子就是:

    1
    ["SELECT * FROM users WHERE email=", " AND password=", ""]
  • 将模板字符串的变量部分,转换成数组,解析后传给不定参数...exps,也就是:

    1
    ["test@example.com", "hello123"]
  • 先在内部把strings转换成SQL字符串,然后把...exps作为参数,就可以实现一个安全的SQL查询.例如这样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function update() {
    let sql = strings.join('?');
    // 执行数据库更新
    // TODO:
    }

    // 调用非常简洁
    let id = 123;
    let age = 21;
    let score = 'A';

    update`UPDATE users SET age=${age}, score=${score} WHERE id=${id}`;

生成器

ES6引入的新数据类型,一个生成器看上去像一个函数,但可以返回多次.定义JS生成器标准的哥们借鉴了python生成器的概念和语法.

函数在执行过程中如果没有遇到return,控制权无法交回被调用的代码.(函数如果没有显式声明return,默认都会有一个return undefined).生成器与函数类似,定义如下:

1
2
3
4
5
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}
  • function*,定义时比函数多一个*
  • 除了return,还可以用yield返回多次

举个例子, 斐波那契数列:

1
0 1 1 2 3 5 8 13 21 34 ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 函数方式生成,返回array
function fib(max) {
let
t,
a = 0,
b = 1,
arr = [0, 1];
while (arr.length < max) {
[a, b] = [b, a + b];
arr.push(b);
}
return arr;
}

fib(5); // [0, 1, 1, 2, 3]
fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

那么跟python类似的,当数据量无限增大,这个函数在内存的占用也会无限增大,所以使用生成器是个更好的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 生成器
function* fib(max) {
let
t,
a = 0,
b = 1,
n = 0;
while (n < max) {
yield a;
[a, b] = [b, a + b];
n ++;
}
return;
}

// 调用生成器一: 使用next()
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}

// 方法二: for...of,不需要自己判断done
for (let x of fib(10)) {
console.log(x); // 依次输出
}

生成器可以看成一个可以记住执行状态的函数.

需要注意的是生成器执行到yeild就会结束,要让它保持生成多个元素,一般都需要加个循环包裹.


JavaScript-函数
http://example.com/2024/11/06/js-function/
作者
Peter Pan
发布于
2024年11月6日
许可协议