1. 函数的基本使用
使用函数声明或者函数表达式创建一个函数
foo(); //foo
bar(); //Uncaught ReferenceError: Cannot access 'bar' before initialization
//函数声明
function foo(){
console.log(foo);
};
//函数表达式
const bar = function(){
console.log('bar');
};
setTimeOut(function(){
//匿名函数
});
- 函数声明会使函数整体提升
- 函数表达式只会提升声明不会提升函数体
2. 作用域和闭包
2.1 函数的作用域
- 函数有自己的作用域
- 通常情况下函数作用域用内的变量存在于函数运行期间,函数执行完毕后销毁
- 函数内可以使用上级作用域的变量,上层作用域无法使用函数内部的变量
const age = 21;
function foo(){
const name = 'ian';
console.log(age); //21
return name;
};
console.log(foo); //ian
console.log(name); //undefined
2.2 闭包 closure
形成闭包的三个条件
- 函数嵌套
- 内部函数引用了外部函数的变量
- 外部函数在外层作用域被调用(形成内部函数的引用)
function outer(){
const name = 'ian';
function inner(){ console.log(name) };
return inner;
}
const bar = outer();
bar(); //ian
outer()函数执行的时候得到inner函数,inner函数中使用了outer作用域中的name导致outer函数执行完无法正常释放,变量bar又对inner有了引用,形成了闭包。闭包实际上提供了一种有外部访问函数内部变量的方法。
2.3 闭包的使用场景
函数柯理化
function add(x){
return function(y){
return x + y;
};
};
const add5 = add(5);
const add3 = add(3);
console.log(add5(1)); //6
console.log(add3(1)); //4
缓存
使用闭包缓存列表进行求和
function sum() {
const list = [];
return function (num) {
if (num) {
list.push(num);
}
else {
//沒有传递参数时返回求和结果
let res = 0;
for (var i = 0; i < list.length; i++) {
res += list[i];
};
return res;
}
};
};
const add = sum();
add(1);
add(2);
add(3);
console.log(add()); //6
使用闭包实现防抖函数
function debounce(fn, await) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, await);
}
}
通过闭包实现变量/方法的私有化
function user(){
const user ={
name: 'ian',
age: 21,
};
return function(){
return name;
}
};
2.4 闭包的优缺点
闭包的优点:
- 提供了外部作用域访问访问函数内部的方法
- 延长了变量的声明周期
- 避免定义全局变量造成污染
闭包的缺点:
闭包延长了变量的生命周期,增加了变量的引用,大量使用闭包有内存泄露的风险
2.5 闭包一定会造成内存泄露吗?
- 内存泄露是指没有被使用的变量一直被引用没有被释放,导致内存一直被占用。
- 闭包不一定会造成内存泄露,只是增加了内存泄露的风险。
下面的代码会造成内存泄露
window.onload=function(){
const btn = document.getElementById('btn');
btn.onclick=function(){
console.log(btn.id);
}
};
避免闭包中内存泄漏的两种方法:
- 将不用的变量设置为null
- 减少不必要的变量
window.onload = function () {
const btn = document.getElementById('btn');
const id = btn.id;
btn.onclick = function () {
console.log(id);
};
btn = null;
};
window.onload = function () {
const btn = document.getElementById('btn');
document.getElementById('btn').onclick = function () {
console.log(document.getElementById('btn').id);
};
};
3. 函数的参数(arguments和arguments.callee)
arguments是一个伪数组,用来获取函数的全部参数
function add(x, y) {
return x + y;
};
function add1() {
let res = 0;
for (let i = 0; i < arguments.length; i++) {
res += arguments[i]
};
return res;
}
arguments.callee指向参数所属的当前执行的函数
function foo() {
console.log(arguments.callee === foo);
};
foo();
将Argumnets转为数组
//使用数组的slice方法将伪数组转为数组
function add() {
const nums = [].slice.call(arguments);
console.log(nums);
}
//使用拓展运算符将伪数组转为数组
function add(x, y) {
const nums = [...arguments];
};
add(1, 2); //[1, 2]
4. 构造函数
4.1 构造函数的使用
- 构造函数是一种特殊的函数,总是与new关键字一起使用,得到一个对象
- 构造函数的声明一般以大写字母开头
function Person(name, age){
this.name = name;
this.age = age;
};
const jack = new Person('jack',21); //{name:'jack', age:21}
const tom = new Person('tom',22); //{name:'tom', age:22}
4.2 构造函数执行的过程
- 创建一个新对象,将this指向这个新对象
- 执行代码为这个对象添加属性
- 返回这个对象
4.3 构造函数的返回值
构造函数默认返回一个新的对象,如果有手动的return情况如下:
- 手动返回基础类型将会被忽略,仍然返回默认的对象
- 手动返回引用类型的数据将覆盖默认的返回值
function Person(name,age){
this.name = name;
this.age =age;
return 'person';
};
const jack = new Person('jack',23); // //{ name: "jack", age: 23 }
function Person1(name,age){
this.name = name;
this.age =age;
return ['name','age'];
};
const tom = new Person1('tom',23); //Array [ "name", "age" ]
5. 立即执行函数 IIFE
立即执行函数就是声明一个匿名函数然后立即执行它
5.1 立即执行函数的语法
function foo(){} ();
这个代码会报错,因为function关键字既可作为函数声明也可作为函数表达式,当以function开头的时候会被认为是函数声明,直接使用()去执行会抛出异常,我们在函数外面包上一个括号使之成为函数表达式。正确的写法如下:
(function(){
console.log('立即执行函数');
}());
(function(){
console.log('立即执行函数');
})();
甚至你可以这样写:
+ function(){}(console.log('这是一个函数表达式'));
- function(){}(console.log('这是一个函数表达式'));
! function(){}(console.log('这是一个函数表达式'));
~ function(){}(console.log('这是一个函数表达式'));
5.2 立即执行函数的作用
- 立即执行函数只会执行一次,而且是匿名函数无法被手动调用
- 立即执行函数会形成一个内部的作用域,用来存放变量和方法,起到保护作用
- 避免对全局变量的污染
5.3 立即执行函数的经典使用场景
//点击按钮输出都是5
const btns = document.getElementsByClassName('btn');
for(var i = 0;i<btns.length;i++){
btns[i].onclick=function(){
console.log(i);
}
};
//使用立即执行函数解决
for (var i = 0; i < btns.length; i++) {
btns[i].onclick = (function (i) {
return function(){
console.log(i);
};
}(i));
};
</script>
使用立即执行函数后,每次for循环都会得到一个新的函数,i从1~5分别被保存到5个不同的函数里,因此点击按钮的时候能依次输出1到5。
6. 递归
递归就是函数调用自身。递归的两个必要条件:
- 函数调用自身
- 设置出口条件
利用递归实现斐波那契数列
function fibonacci(n) {
if (n <= 2) {
return 1
} else {
return fibonacci(n - 1) + fibonacci(n - 2)
}
};
fibonacci(1); //1
fibonacci(2); //1
fibonacci(3); //2
fibonacci(4); //3
fibonacci(5); //5
7. 高阶函数
高阶函数的定义: 函数接收函数作为入参
const nums = [1,2,3];
nums.filter(function(n){ returen n>2} );
高阶函数的好处是将具体的计算以函数参数的形式抽离到函数外部,能更灵活的增强函数的功能
8. 函数式编程
- 函数编程的前提是函数必须幂等:同样的输入有同样的输出
- 函数式编程设计到的知识点有函数柯理化(curring)、函数组合(compose)等
- 更详细的请参考:JavaScript中的函数式编程
9. eval
eval接收一个字符串,将这个字符串当作JS语句执行
const a = 1;
eval('console.log(a)'); //1
非严格模式下eval能声明和复写变量的值
let a = 1;
eval('a=2; b = 3;')
console.log(a,b); // 2,3
使用别名调用eval的时候,eval使用全局作用域
const exec = eval;
const a = 1;
funtion foo(){
const a = 2;
exec('console.log(a)'); //1
};
总结
函数的基础使用
-
使用函数表达式或者函数声明去创建一个函数
-
函数声明整体提升,函数表达式只提升变量
-
匿名函数
-
构造函数
- 构造函数返回一个对象,使用new操作符执行构造函数
- 构造函数执行的过程:1. 生成一个对象将this绑定到这个对象 2. 执行构造函数的代码 3.返回结果
- 构造函数默认返回一个对象,手动返回应用类型的数据将代替默认的返回结果
-
闭包
- 闭包产生的三个条件:1. 函数嵌套 2.内部函数使用了外部函数作用域内的变量 3.被返回的内部函数在外部作用域有被引用
- 闭包的主要功能: 1. 提供了函数外部访问函数内部作用域的方法 2. 延长了变量的声明周期,外部函数执行完里面的变量不会立即销毁
- 闭包的使用场景:缓存、保存私有属性/方法 、柯理化、防抖
- 闭包的缺点: 增加了内存的占用,外部函数没执行一次都会增加一次引用,可能会造成内存泄漏
- 如何避免闭包造成内存泄漏: 不使用的变量手动设置为null
-
函数的arguments和arguments.callee
- arguments用来获取函数的全部参数是个伪数组
- 使用Array.prototype.slice(argument)或者[…arguments]将伪数组转为数组
- arguments.callee指向参数所属的正在执行的函数
-
立即执行函数(IIFE)
- 立即执行函数用途:1. 避免污染全局变量 2.保存私有属性/方法
- 常见的立即执行函数语法: (function(){}())、 (function(){})()
-
递归
- 递归就是函数调用自身
- 递归的2个必要条件: 1.函数调用自身 2.设置出口
-
eval
- eval接受一个字符串,讲字符串当作js语句执行
- 非严格模式下eval可以声明和修改变量
- 使用别名调用eval时,eval的作用域为全局