目标
- 能够说出函数的多种定义和调用方式
- 能够说出和改变函数内部this的指向
- 能够说出严格模式的特点
- 能够把函数作为参数和返回值传递
- 能够说出闭包的作用
- 能够说出递归的两个条件
- 能够说出深拷贝和浅拷贝的区别
函数的定义
函数声明方式function
function fn() {}
函数表达式(匿名函数)
var fun = function() {}
new Function() (构造函数)
这种方式比较麻烦,不符合我们平时书写习惯,但是通过这个例子我们会得出一个结论:所有的函数都是Function函数的实例对象
函数也属于对象(万物皆对象)
// new Function('参数1', '参数2', '函数体')
var f = new Function('a', 'b', 'console.log(a + b)');
f(1, 2);
函数的调用
// 1. 普通函数
function fn () {
console.log('111');
}
fn(); // 或者fn.call();
// 2. 对象的方法
var o = {
sayHi: function() {
console.log('222');
}
}
o.sayHi();
// 3. 构造函数
function Star() {}
new Star();
// 4. 绑定事件函数
btn.onclick = function() {}
// 点击了事件就可以调用函数
// 5. 定时器函数
setInterval(() => {}, 1000) // 这个函数是定时器每隔一秒调用一次
// 6. 立即执行函数
(function() {
console.log('333');
})()
// 立即执行函数是自动的调用
this指向问题
// 函数不同的调用方式决定了this指向不同
// 1. 普通函数
function fn () {
console.log('普通函数的this' + this);
// 此时this是指向函数的调用者,即window
}
fn(); // 或者fn.call();
// 2. 对象的方法
var o = {
sayHi: function() {
console.log(this);
// 此时this指向的是o这个对象
}
}
o.sayHi();
// 3. 构造函数
function Star() {
console.log(this)
// 构造函数的this指向s这个实例对象,原型对象里面的this,指向的也是s这个实例对象
}
var s = new Star();
// 4. 绑定事件函数
btn.onclick = function() {
console.log(this)// 这里的this指向的是函数的调用者即btn这个元素
}
// 点击了事件就可以调用函数
// 5. 定时器函数
setInterval(() => {}, 1000) // 这个函数是定时器每隔一秒调用一次
// 定时器的this指向的是window
// 6. 立即执行函数
(function() {
console.log('333');
})()
// 立即执行函数是自动的调用
// 立即执行函数this指向的也是window
改变函数内部的this指向
Js为我们专门提供了一些函数方法来帮我们更优雅的处理函数内部this的指向问题,常用的有bind()
、call()
、apply()
三种方法
1. call()
语法: fun.call(thisArg, arg1, arg2, ...)
参数:
- thisArg: 当前调用函数this的指向对象
- arg1,arg2: 传递的其他参数
返回值:
使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。
call()
方法两个作用:一个是调用函数,简单理解为调用函数的方式,第二是可以改变函数内部的this指向.
改变函数内部this指向
function fn(x, y) {
console.log('111');
console.log(this); // 正常来说这里打印的this的指向应该是window
console.log(x + y); // 3
}
var obj = {
name: 'li',
}
// 1. 调用函数
fn.call();
// 2. 改变函数内部this的指向
fn.call(obj, 1, 2); // 通过call方法,我们可以实现把fn函数内部的this指向obj这个对象,这就是call的强大之处
call
实现继承。
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}
console.log(new Food('cheese', 5).name);
// expected output: "cheese"
上面的例子中,通过call
改变了Product
的this指向,让它指向了Food
,那么Product
的所有属性都被Food
所继承
如果没有传递第一个参数,this 的值将会被绑定为全局对象。
var sData = 'Wisen';
function display() {
console.log('sData value is ', this.sData);
}
display.call(); // sData value is Wisen
// 如果没有传递第一个参数,this 的值将会被绑定为全局对象,但是这是非严格模式下的情况
// 如果是严格模式下,sData的值为 undefined
call()
方法的作用和 apply()
方法类似,区别就是call()
方法接受的是参数列表,而apply()
方法接受的是一个参数数组。
2. apply()
-
apply()
方法调用一个函数,简单理解为调用函数的方式,但是可以改变函数的this指向
语法:fun.apply(thisArg, [argsArray])
参数:
- thisArg:在fun函数运行时指定的this值
- argsArray:传递的值,必须包含在数组里面
- 返回值就是函数的返回值,因为它就是调用函数
- apply的主要应用:比如说我们可以用apply借助数学内置对象求最大值
你也可以使用 arguments
对象作为 argsArray
参数。 arguments
是一个函数的局部变量。 它可以被用作被调用对象的所有未指定的参数。 这样,你在使用apply函数的时候就不需要知道被调用对象的所有参数。 你可以使用arguments来把所有的参数传递给被调用对象。 被调用对象接下来就负责处理这些参数。
改变this指向
var person = {
fullName: function() {
return this.firstName + " " + this.lastName;
}
}
var person1 = {
firstName: "Bill",
lastName: "Gates",
}
person.fullName.apply(person1);
获取数组的最大最小值
对于一些需要写循环以遍历数组各项的需求,我们可以用apply完成以避免循环。
const arr = [10, 2, 88, 5];
// 传统方法
let maxNum = arr[0],
minNum = arr[0];
arr.forEach((item, i) => {
maxNum = Math.max(maxNum, arr[i])
minNum = Math.min(minNum, arr[i])
})
console.log(maxNum) // 88
console.log(minNum) // 2
// apply方法
const maxNum = Math.max.apply(null, arr);
const minNum = Math.min.apply(null, arr);
console.log(maxNum) // 88
console.log(minNum) // 2
apply 方法的最主要的作用是改变当前函数的 this
指向,还有一个作用就是将数组中的元素一个一个的当做参数传入方法,apply
方法和 call
方法基本相同, 此处 Math.max.apply(null, arr)
和 Math.max.apply(null, [10, 2, 88, 5])
相同, 所以当我们调用 apply
方法的时候,相当于把数组中的元素挨个当做参数传入方法中,这也就解决了 Math.max(min)
不能直接获取数组中的最大(小)值问题了
用 apply
将数组各项添加到另一个数组
我们可以使用push将元素追加到数组中。由于push接受可变数量的参数,所以也可以一次追加多个元素。
但是,如果push的参数是数组,它会将该数组作为单个元素添加,而不是将这个数组内的每个元素添加进去,因此我们最终会得到一个数组内的数组
如果不想这样呢?concat符合我们的需求,但它并不是将元素添加到现有数组,而是创建并返回一个新数组。 然而我们需要将元素追加到现有数组,这时候用apply正合适
var array = ['a', 'b'];
var elements = [0, 1, 2];
array.push.apply(array, elements); // ["a", "b", 0, 1, 2]
3. bind()
bind()
方法不能调用函数,但是也可以改变函数内部的this指向
这个方法在我们后面的开发中会经常用到
语法:fun.bind(thisArg, arg1, arg2, ...)
参数:
- thisArg:在fun函数运行是指定的this值
- arg1,arg2:其他参数
返回值:
返回由指定的this值和初始化参数改造的原函数的拷贝
var o = {
name: '小米'
}
function fn () {
console.log(this);
}
const f = fn.bind(o); // 不会调用函数,但是可以改变原函数内部的this指向
// 返回的是改变this指向之后产生的新函数
f();
// 如果有的函数我们不需要立即调用,又想改变函数内部this指向,此时用bind是最合适的
// 比如我们有一个按钮,点击了之后就禁用按钮,三秒之后开启这个按钮
var btn = document.querySelector('button');
btn.onclick = function() {
this.disabled = true;
setTimeout(function() {
this.disabled = false;
// 这里的this指向的是window,所以直接在这里这么写是不对的。
}.bind(this), 3000)
// 我们把定时器这个函数绑定一个bind,并且把定时器这个函数的this指向btn
}
总结
-
call
和apply
会调用函数,并且改变函数内部this指向 -
call
和apply
传递的参数不一样,call传递arg1,arg2形式,apply必须是数组形式 -
bind
不会调用函数,只会改变函数内部this指向
应用场景 -
call
经常用作继承 -
apply
经常跟数组有关系,比如借助数学对象实现求数组的最大最小值。 -
bind
不调用函数,但是还想改变内部this指向,比如定时器内部改变this指向。
严格模式
- Js除了提供正常的模式外,还提供了
严格模式(strict mode)
。ES5的严格模式是采用具有限制性Js变体的一种方式,即在严格模式下运行Js代码。 - 严格模式在IE10以上才会被支持,旧版本会被忽略
- 严格模式对正常的Js语义做了一些更改:
1.消除了Js语法的不合理、不严谨之处
2.消除代码运行的一些不安全之处,保证代码运行的安全
3.提高编译器效率,增加运行速度。
4.禁用了ECMAScript的未来版本中可能会定义的一些语法,为未来新版本的Js做好铺垫,比如一些保留字如:class,enum,extends,import,super不能做变量名
为脚本开启严格模式
<script>
'use strict';
// 下面的js代码会按照严格模式执行代码
</script>
<script>
function(){
'use strict';
}()
</script>
为函数开启严格模式
// 为某个函数开启严格模式
function fn() {
'use strict';
// 只会为fn函数执行严格模式
}
function fun() {}
严格模式中的变化
- 在正常变量中,如果变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,变量必须先用var声明,然后再使用
- 严禁删除已经声明的变量,用delete 删除变量会报错。
this指向问题
- 全局作用域下this指向不是window了,而是undefined
- 以前构造函数不加new也是可以当做普通函数调用,在严格模式下this指向的是undefined,此时就会报错了。
- 定时器中的this指向,以前指向的是window,在严格模式下还是window
- 事件、对象中this指向的还是调用者。
函数变化
- 严格模式下函数参数不允许重名。
- 函数必须声明在顶层,新版本的Js会引入“块级作用域”(ES6中已引入),为了与新版本接轨,不允许在非函数的代码块中声明函数。
高阶函数
高阶函数是对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值输出
- 如果说函数它接收的参数是一个函数,它就是一个高阶函数
function fn(a, b, callback) {
console.log(a + b);
callback && callback();
}
fn(1, 2, function() {
console.log('我是最后调用的');
})
- 如果这个函数的返回值不是一个普通值,而是一个函数,它也是一个高阶函数
闭包
变量可以根据作用域的不同分为两种:全局变量和局部变量
- 1.函数内部可以使用全局变量
- 2.函数外部不可以使用局部变量
- 3.当函数执行完毕,本作用域内的局部变量会销毁
闭包(closure)是指有权访问另一个函数作用域中变量的函数。
上面这句话抓重点,闭包首先是一个函数,其次一个作用域可以访问另外一个函数内部的局部变量这就是闭包
function fn () {
var num = 10;
function fun() {
console.log(num);
}
fun();
}
fn();
// fun这个函数作用域,访问了另外一个函数fn里面的局部变量num
// 说明此时fn就是一个闭包函数
function fn () {
var num = 10;
function fun() {
console.log(num);
}
return fun;
}
var f = fn();
f();
// 我们fn外部的作用域可以访问fn内部的局部变量
// 类似于
// var f = function fun() {
// console.log(num);
// }
// 上面的代码也可以这样修改
// 闭包就是一个典型的高阶函数
function fn () {
var num = 10;
return function() {
console.log(num);
}
}
var f = fn();
f();
闭包的主要作用:延伸了变量的作用范围
闭包案例
1.循环注册点击事件
<ul class="nav">
<li>榴莲</li>
<li>芒果</li>
<li>非鱼罐头</li>
<li>大猪蹄子</li>
</ul>
<script>
// 1. 我们可以利用动态添加属性的方式
var lis = document.querySelector('.nav').querySelectorAll('li');
for (let i = 0; i < lis.length; i++) {
lis[i].index = i;
lis[i].onclick = function() {
console.log(this.index);
}
}
// 2. 利用闭包的方式
for (let i = 0; i < lis.length; i++) {
// 利用for循环创建了4个立即执行函数
// 立即执行函数也称为小闭包,因为立即执行函数里面的任何一个函数都可以使用它的i变量。
(function(i) {
lis[i].onclick = function () {
console.log(i);
}
})(i);
}
// 上面这个例子显然第一种方法会更好,所以说闭包不一定是更好的(像这个例子必须要点击后才会释放内存,但是一直不点击,就会造成内存泄漏)。
</script>
2.定时器中的闭包
// 实现3秒之后,打印所有li元素的内容
var lis = document.querySelector('.nav').querySelectorAll('li');
for (let i = 0; i < lis.length; i++) {
(function(i){
setTimeout(function(){
console.log(lis[i].innerHTML);
}, 3000)
})(i)
}
3.计算打车价格
// 闭包应用-计算打车价格
// 打车起步价13(3公里内),之后每多一公里增加5块钱,用户输入公里数就可以计算打车价格
// 如果有拥堵情况,总价格多收取10元拥堵费
var car = (function(){
var start = 13;
var total = 0;
return {
price: function(n) {// 正常的总价
if(n <= 3) {
total = start;
} else {
total = start + (n - 3) * 5;
}
return total;
},
yd: function(flag) {// 拥堵之后的费用
return flag ? total + 10 : total;
}
}
})()
car.price(5);
car.yd(true)
递归
- 如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。
- 简单说来就是函数内部自己调用自己。
- 递归函数的作用和循环效果一样
function fn() {
fn();
}
fn();
// 此时就会发生死循环,造成栈溢出的错误。
由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件 return
var num = 1;
function fn() {
console.log('我要打印6句话');
if(num == 6) {
return;
}
num ++;
fn();
}
fn();
案例一
求123...*n 阶乘
function fn(n) {
if(n == 1) {
return 1;
}
return n * fn(n - 1);
}
fn(3);
案例二
递归求斐波那契数列(1、1、2、3、5、8、13、21)
function fn(n) {
if(n <= 2) {
return 1
}
return fn(n - 1) + fn(n - 2);
}
fn(6) // 8
案例三
利用递归求:根据id返回对应的数据对象
// 我们想实现输入id号就可以返回数据对象
var data = [{
id: 1,
name: '家电',
goods: [{
id: 11,
gname: '冰箱',
},{
id: 12,
gname: '洗衣机'
}]
},{
id: 2,
name: '服饰'
}]
function getId(json, id){
var o = {};
json.forEach(item => {
if(item.id == id) {
o = item;
return item;
// 2.我们想要得到goods的数据
}else if(item.goods && item.goods.length > 0) {
o = getId(item.goods, id);
}
});
}
getId(data, 1);
getId(data, 11);