函数进阶
目标
- 能够说出函数的多种定义和调用方式
- 能够说出和改变函数内部 this 的指向
- 能够说出严格模式的特点
- 能够把函数作为参数和返回值传递
- 能够说出闭包的作用
- 能够说出递归的两个条件
- 能够说出深拷贝和浅拷贝的区别
目录
- 函数的定义和调用
- this
- 严格模式
- 高阶函数
- 闭包
- 递归
1. 函数的定义和调用
1.1 函数的定义方式
-
函数声明方式 function 关键字(命名函数)
// 函数声明方式 function 关键字(命名函数) function fn() {}
-
函数表达式(匿名函数)
// 函数表达式(匿名函数) var fun = function () {};
-
new Function()
// 利用 new Function('参数1','参数2','函数体'); var fn = new Function("参数1", "参数2", "...", "函数体");
- function 里面参数都必须是字符串格式
- 第三种方式执行效率低,也不方便书写,因此较少使用
- 所有函数都是 function 的实例(对象)
- 函数也属于对象
<script> // 1. 函数声明方式 function 关键字(命名函数) function fn() {} // 2. 函数表达式(匿名函数) var fun = function () {}; // 3. 利用 new Function('参数1','参数2','函数体'); var f = new Function("a", "b", "console.log(a+b)"); // 3 f(1, 2); // 4. 所有函数都是 Function 的实例(对象) console.dir(f); // 5, 函数也属于对象 万物皆对象 console.log(f instanceof Object); // true </script>
1.2 函数的调用方式
-
普通函数
-
对象的方法
-
构造函数
-
绑定事件函数
-
定时器函数
-
立即执行函数
<script> // 1. 普通函数 function fn() { console.log("人生的巅峰"); } fn(); fn.call(); // 2. 对象的方法 var obj = { name: "张三", age: 18, sayHi: function () { console.log("人生的巅峰"); }, }; obj.sayHi(); // 3. 构造函数 function Star() {} new Star(); // 4. 绑定事件函数 btn.onclick = function () {}; // 点击了按钮就可以调用这个函数 // 5. 定时器函数 setTimeout(function () { console.log("定时器函数"); }, 2000); // 这个函数是定时器自动1秒调用一次 // 6. 立即执行函数 是自动调用 (function () { console.log("立即执行函数"); })(); </script>
2. this
2.1 函数内部 this 的指向
这些 this 的指向,是当我们调用函数的时候确定的。调用方式的不同决定了 this 的指向不同
一般指向我们的调用者
调用方式 | this 指向 |
---|---|
普通函数调用 | window |
构造函数调用 | 实例对象 原型对象里面的方法也指向实例对象 |
对象方法调用 | 该方法所属对象 |
事件绑定方法 | 绑定事件对象 |
定时器函数 | window |
立即执行函数 | window |
<body>
<button>按钮</button>
</body>
<script>
// 1. 普通函数 this 指向 window
function fn() {
console.log("普通函数的this" + this); // 普通函数的this[object Window]
}
// window.fn();
fn();
// 2. 对象的方法 this 指向的是对象 obj
var obj = {
name: "张三",
age: 18,
sayHi: function () {
console.log("对象方法的this" + this); // 对象方法的this[object Object]
},
};
obj.sayHi();
// 3. 构造函数 this 指向 ldh 这个实例对象 原型对象里面的this 指向的也是ldh 这个实例对象
function Star() {
console.log("构造函数的this" + this);
}
Star.prototype.sing = function () {};
var ldh = new Star();
// 4. 绑定事件函数 this 指向函数的调用者 btn这个按钮
var btn = document.querySelector("button");
btn.onclick = function () {
console.log("绑定事件函数的this" + this); // 绑定事件函数的this[object HTMLButtonElement]
}; // 点击了按钮就可以调用这个函数
// 5. 定时器函数 this 指向的也是 window
setTimeout(function () {
console.log("定时器函数的this" + this); // 定时器函数的this[object Window]
}, 2000); // 这个函数是定时器自动1秒调用一次
// 6. 立即执行函数 是自动调用
(function () {
console.log("立即执行函数的this" + this); // 立即执行函数的this[object Window]
})();
</script>
2.1 改变函数内部 this 指向
JavaScript 为我们专门提供了一些函数方法来帮我们更优雅的处理函数内部 this 的指向问题,常用的有bind()
、call()
、apply()
三种方法。
1. call()
方法
call()
方法调用一个对象。简单理解为调用函数的方式,但是它可以改变函数 this 的指向
fun.call(thisArg, arg1, arg2, ...)
<script>
// call()
var o = {
name: "andy",
};
function fn(a, b) {
console.log(this); // {name: 'andy'}
console.log(a + b); // 3
}
fn.call(o, 1, 2);
// call 第一个可以调用函数 第二个可以改变函数内的 this 指向
// call 的主要作用可以实现继承
function Father(uname, age, sex) {
this.name = uname;
this.age = age;
this.sex = sex;
}
function Son(uname, age, sex) {
Father.call(this, uname, age, sex);
}
var son = new Son("刘德华", 18, "男");
console.log(son); // Son {name: '刘德华', age: 18, sex: '男'}
</script>
2.apply()
apply()
方法调用一个函数,简单理解为调用函数的方式,但是它可以改变函数的 this 指向。
fun.apply(thisArg, [argsArray])
<script>
// apply()
var o = {
name: "andy",
};
function fn(a, b) {
console.log(this); // {name: 'andy'}
console.log(a + b); // 3
}
fn.apply(o, [1, 2]);
// 1. apply 第一个可以调用函数 第二个可以改变函数内的 this 指向
// 2. 但是它的参数必须是数组(伪数组)
// 3. apply 的主要应用 比如:我们可以利用 apply 借助于数学内置求最大值
var arr = [1, 33, 34, 22, 6];
var max = Math.max.apply(Math, arr);
var min = Math.min.apply(Math, arr);
console.log(max); // 34
console.log(min); //1
</script>
3.bind()
方法
bind()
方法不会调用函数,但是能改变函数内部 this 指向
fun.bind(thisArg, arg1, arg2, ...)
-
thisArg
:在 fun 函数运行时指定的 this 值 -
arg1, arg2, ...
:传递的其他参数 -
返回由指定的 this 值和初始化参数改造的原函数拷贝
<body> <button>按钮</button> </body> <script> // bind() var o = { name: "andy", }; function fn(a, b) { console.log(this); console.log(a + b); // 3 } var f = fn.bind(o, 1, 2); f(); // 1. 不会调用原来的函数 可以改变原来函数内部的 this 指向 // 2. 返回的是原函数改变this之后产生的新函数 // 3. 如果有的函数不需要立即调用,但是又想改变这个函数内部this指向此时用bind // 4. 我们有一个按钮,当我们点击了之后,就禁用这个按钮,3秒钟之后开启这个按钮 var btn = document.querySelector("button"); btn.onclick = function () { this.disabled = true; // 这个this 指向的是 btn 这个按钮 setTimeout( function () { // this.disabled = false; // 定时器函数里面的this 指向的是window this.disabled = false; // 此时定时器函数里面的 this 指向的是 btn }.bind(this), // 这个this指向的是btn这个对象 3000 ); }; </script>
2.2 call
、apply
、bind
总结
- 相同点
- 都可以改变函数内部的 this 指向
- 区别点
call
和apply
会调用函数,并且改变函数内部 this 指向call
和apply
传递的参数不一样,call
传递参数aru1,aru2,..
形式,apply
必须数组形式[arg]
bind
不会调用函数,可以改变函数内部 this 指向
- 主要应用场景
call
主要做继承apply
经常跟数组有关,比如借助于数学对象实现数组最大值和最小值bind
不调用函数,但是还想改变 this 指向,比如改变定时器内部的 this 指向
3. 严格模式
3.1 什么是严格模式
JavaScript 除了 提供正常模式外,还提供了严格模式(strict mode)。ES6 的严格模式是采用具有限制性 JavaScript 变体的一种方式,即在严格的条件下运行 JS 代码。
严格模式在 IE 10 以上版本的浏览器中才会被支持,旧版本浏览器中会被忽略
严格模式对正常的 JavaScript 语义做了一些更改:
- 消除了 JavaScript 语法的一些不合理、不严谨之处,减少了一些怪异行为。
- 消除了代码运行的一些不安全之处,保证代码运行的安全
- 提高编译器效率,增加运行速度
- 禁用了在 ECMAScript 的未来版本中可能定义的一些语法,为未来新版本的 JavaScript 做好铺垫,比如一些保留字,如:
class
、enum
、expor
、extends
、import
、super
不能做变量名
3.2 开启严格模式
严格模式可以应用到整个脚本或个别函数中,因此在使用时,我们可以将严格模式分为脚本开启严格模式和为函数开启严格模式两种情况。
1. 为脚本开启严格模式
为整个脚本文件开启严格模式,需要在所有语句之前放一个特定语句 “use strict”😭 或 ‘use strict’ )
有的 script 基本是严格模式,有的 script 脚本是正常模式,这样不利于文件合并,所以可以将整个脚本文件放在一个立即执行的匿名函数之中。这种独立创建一个作用域而不影响其他 script 脚本文件。
<script>
// 为整个脚本(script标签)开启严格模式
"use strict";
// 下面 js 代码就会按照严格模式执行代码
</script>
<script>
(function () {
"use strict";
// 为我们整个脚本开启严格模式
})();
</script>
2. 为函数开启严格模式
要给整个函数开启严格模式,需要把 “use strict”:(或 ‘use strict’)声明放在函数体所有语句之前
<script>
// 为某个函数开启严格模式
// 此时只是给 fn 函数开启严格模式
function fn() {
"use strict";
// 下面的代码按照严格模式执行
}
function func() {
// 里面的还是按照普通模式执行;
}
</script>
3.3 严格模式中的变化
严格模式对 JavaScript 的语法和行为,都做了一些改变
1. 变量规定
- 在正常模式下,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,变量都必须先用 var 命名声明,然后再使用
- 严禁删除已经声明变量。例如:
delete x;
语法是错误的。
2. 严格模式下 this 指向问题
-
以前在全局作用域函数中的 this 指向 window 对象
-
严格模式下全局作用域中函数的 this 是 undefined
-
以前构造函数时不加 new 也可以调用,当普通函数,this 指向全局对象
-
严格模式下,如果构造函数不加 new 调用,this 会报错
-
new 实例化的构造函数指向创建的对象实例
-
定时器 this 还是指向 window
-
事件、对象还是指向调用者
<script> "use strict"; function fn() { console.log(this); // undefined } fn(); // 严格模式下,如果 构造函数不加 new 调用,this 会报错 function Star() { this.sex = "男"; } // Star(); var ldh = new Star(); console.log(ldh.sex); // 定时器this指向还是window setTimeout(function () { console.log(this); }, 2000); </script>
3. 函数变化
-
函数不能有重名的参数
-
函数必须声明在顶层,新版本的 JavaScript 会引入 “块级作用域”(ES6 中已引入)。为了与新版本接轨,不允许在非函数的代码块内声明函数。
<script> "use strict"; function fn(a, a) { console.log(a + a); // 在严格模式下是会报错的 } fn(1, 2); </script>
4. 高阶函数
高阶函数是对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值输出。
<script>
function fn(callback) {
callback && callback();
}
fn(function () {
alert("hi");
});
function foo() {
return function () {};
}
foo();
</script>
此时 fn 和 foo 就是一个高阶函数
函数也是一种数据类型,同样可以作为参数,传递给另一个参数使用。最典型的就是回调函数。
5. 闭包
5.1 变量作用域
变量根据作用域的不同分为两种:全局变量和局部变量。
- 函数内部可以使用全局变量
- 函数外部不可以使用局部变量
- 当函数执行完毕,本作用域内的局部变量会销毁
5.2 什么是闭包
闭包(closure)指有权访问另一个函数作用域中变量的函数。 ———— JavaScript 高级程序设计
简单理解就是:一个作用域可以访问另外一个函数内部的局部变量。
<script>
// 闭包:我们 fun 这个函数作用域 访问了另外一个函数 fn 里面的局部变量 num
function fn() {
var num = 10;
function fun() {
console.log(num); // 10
}
fun();
}
fn();
</script>
5.3 闭包总结
1. 闭包是什么?
- 闭包是一个函数(一个作用域可以访问另外一个函数的局部变量)
2. 闭包的作用是什么?
- 延伸变量的作用范围
6. 递归
6.1 什么是递归?
如果一个函数在内部都可以调用其本身,那么这个函数就是递归函数。
简单理解:函数内部自己调用自己,这个函数就是递归函数。
递归函数的作用和循环效果一样
由于递归很容易发生 “栈溢出” 错误 (stack overflow),所以必须要加退出条件 return
<script>
var num = 1;
function fn() {
console.log("我要打印6句话");
if (num == 6) {
return; // 递归里面必须加退出条件
}
num++;
fn();
}
fn();
</script>
6.2 利用递归求数学题
-
利用递归函数求 1~n 的阶乘 1 * 2 * 3 * 4 * … n
<script> // 利用递归函数求 1~n 的阶乘 1 * 2 * 3 * 4 * .. n function fn(n) { if (n == 1) { return 1; } return n * fn(n - 1); } console.log(fn(3)); // 6 console.log(fn(4)); // 24 // 详细思路 假如用户输入的是 3 // return 3 * fn(2) // return 3 * (2 * fn(1)) // return 3 * (2 * 1); // return 3 * 2 // return 6 </script>
-
求斐波那契数列
<script> // 利用递归函数求斐波那契数列(兔子序列) 1、1、2、3、5、8、13、21......... // 用户输入一个数字 n 就可以求出 这个数字对应的兔子序列值 // 我们只需要知道用户输入的n 的前面两项 (n-1 n-2) 就可以计算出n对应的序列值 function fb(n) { if (n == 1 || n == 2) { return 1; } return fb(n - 1) + fb(n - 2); } console.log(fb(6)); // 8 // 详情思路分析 // return fb(5) + fb(4) // return fb(4) + fb(3) + fb(3) + fb(2) // return fb(3) + fb(2) + fb(2) + fb(1) + fb(2) + fb(1) + 1 // return fb(2) + fb(1) + 1 + 1 + 1 + 1 + 1 + 1 // return 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 </script>
6.3 利用递归求:根据 id 返回对应的数据对象
<script>
var data = [
{
id: 1,
name: "家电",
goods: [
{
id: 11,
gname: "冰箱",
},
{
id: 12,
gname: "洗衣机",
},
],
},
{
id: 2,
name: "服饰",
},
];
// 我们想要做输入 id 号,就可以返回的数据对象
// 1. 利用我们的 forEach 去遍历里面的每一个对象
function getId(json, id) {
var o = {};
json.forEach(function (item) {
if (item.id == id) {
// console.log(item);
o = item;
return item;
// 2. 我们想要得到里面的数据 11 12 可以利用递归函数
// 里面应该有 goods 这个数组并且数组的长度不为 0
} else if (item.goods && item.goods.length > 0) {
o = getId(item.goods, id);
}
});
return o;
}
console.log(getId(data, 1));
console.log(getId(data, 2));
console.log(getId(data, 11));
console.log(getId(data, 12));
</script>
6.4 浅拷贝和深拷贝
-
浅拷贝只是拷贝一层,更深层次对象级别的只拷贝引用
-
深拷贝拷贝多层,每一级别的数据都会拷贝
<script> var obj = { id: 1, name: "andy", msg: { age: 18, }, color: ["pink", "red"], }; var o = {}; function deepCopy(newObj, oldObj) { for (var k in oldObj) { // 判断我们的属性值属于哪种属性类型 // 1. 获取属性值 oldObj[k] var item = oldObj[k]; // 2. 判断这个是否是数组 if (item instanceof Array) { newObj[k] = []; deepCopy(newObj[k], item); } // 3. 判断这个值是否是对象 else if (item instanceof Object) { newObj[k] = {}; deepCopy(newObj[k], item); } // 4. 属于简单数据类型 else { newObj[k] = item; } } } deepCopy(o, obj); console.log(o); </script>
-
Object.assign(target,..sources);
ES6 新增方法可以浅拷贝