今日学习任务
- 1.函数的四种调用方式
- 重点掌握this指向的三种情况
- a.普通函数
- b.对象方法
- c.构造函数
- d.上下文模式
- 2.函数调用的上下文模式
- a.call()
- b.apply()
- c.bind()
- 上下文模式经典场景
01-函数的三种调用方式(this指向)
1.复习函数三种调用方式:普通函数 对象方法 构造函数
- 重点:理解this关键字作用:
谁调用这个函数,this指向谁
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
/*
函数三种执行模式 : 全局函数 、 对象方法 、 构造函数
this : 谁 `调用` 我,我就指向谁
1. 全局函数 : this指向window
2. 对象方法 : this指向对象
3. 构造函数 : this指向new创建的空对象
*/
//1.全局函数
function fn(){
console.log('111111');
console.log(this);
};
fn();//window.fn()
//2.对象的方法
let obj = {
name:'班长',
sayHi:function(){
console.log('我是胖胖又可爱的得小又又');
console.log(this);
}
};
obj.sayHi();
//将fn的地址赋值给sayHi
obj.sayHi = fn;
//此时this指向obj,this指向跟声明没有关系。取决于函数是如何调用的
obj.sayHi();
//3.构造函数
function Person(name,age){
//(1)创建一个空对象 (2)this指向这个对象 (3)执行赋值代码 (4)返回这个对象
//this :指向new创建的哪个对象
console.log(this);
this.name = name;
this.age = age;
};
let p1 = new Person();//构造函数
//没有加new,以全局函数方式执行。此时this就是window,函数里面其实是给window添加属性(全局变量)
Person('张三',18);//全局函数
console.log(name);
console.log(age);
</script>
</body>
</html>
02-函数调用的上下文模式
了解上下文模式注意点
- a.函数上下文三个方法:
call()
apply()
bind()
,它们定义在Function构造函数的原型中 - b.如果将this指向修改为值类型:(number,boolean,string),则函数会自动帮我们包装成对应的引用类型(基本包装类型)
- 值类型:
'123'
,1
,true
- 基本包装类型:
String('123')
,Number(1)
,Boolean(true)
- 值类型:
1.1-函数执行的上下文模式
作用:可以动态修改函数中的this指向
-
call() apply() bind()
异同点
- 相同之处:都可以修改函数中this指向
- 不同点:传参方式不同
- call()语法:
函数名.call(this修改后的指向,arg1,arg2…………)
- apply()语法:
函数名.apply(this修改之后的指向,伪数组或者数组)
- bind()语法:
函数名.bind(this修改后的指向,arg1,arg2....)
- bind()语法并不会立即执行函数,而是返回一个修改指向后的新函数,常用于回调函数
- bind()语法:
- call()语法:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
/*
1.函数三种执行方式 :
全局函数 : this指向window
对象方法: this指向对象
构造函数 : this指向new创建的对象
共同的特点: this的指向无法动态修改
2.函数上下文模式 :
2.1 作用: 可以动态修改函数中的this
2.2 语法: 有3种写法,作用都是一样改this,应用场景不同
a. 函数名.call(修改的this,arg1,arg2…………)
* 适用于函数原本形参 <= 1
b. 函数名.apply(修改的this,[数组或伪数组])
* 适用于函数原本形参 >= 2
c. 函数名.bind(修改的this,arg1,arg2…………)
* 特点:不会立即执行这个函数,而是返回修改this后的新函数
* 适用于不会立即执行的函数 : 事件处理函数,定时器函数
*/
// function fn(){
// //三种执行模式this无法动态修改
// //this = {age:18};
// console.log(this);
// };
// fn();//this:window
/* 上下文模式 */
function fn(a,b){
console.log(this);
console.log(a+b);
};
//a. 函数名.call(修改的this,arg1,arg2…………)
//应用场景: 适用于函数原本形参 <= 1
fn(10,20);//this:window
fn.call({age:18},100,200);
//b. 函数名.apply(修改的this,[数组或伪数组])
//应用场景: 适用于函数原本形参 >=2
fn.apply({age:88},[20,30]);
//c. 函数名.bind(修改的this,arg1,arg2…………)
//特点:这个函数不会立即执行,而是返回一个修改this之后的新函数
//应用场景 : 事件处理函数,定时器
let newFn = fn.bind({name:'坤坤'});
newFn(50,60);
//4. 定时器中的this一定是指向window
// 定时器:一段代码间隔事件执行 setTimeout(一段代码,间隔时间)
//4.1 具名函数
let test = function(){
console.log('我是具名函数');
console.log(this);
};
let newTest = test.bind({name:'张三'})
setTimeout(newTest,3000);
//4.2 匿名函数
setTimeout(function(){
console.log('我是定时器中的函数');
console.log(this);
}.bind({name:'李四'}),2000);
</script>
</body>
</html>
//2.3 bind();
//语法: 函数名.bind(this修改后的指向,arg1,arg2....);
let obj = {
name:"文聪兄"
};
function getSum(a,b){
console.log(this);
console.log(a+b);
}
getSum(10,20);//this指向window
let fn = getSum.bind(obj); //bind()不会执行这个函数,而是会返回一个修改了this的函数.
fn(100,200); //此时这个fn,就相当于是修改了this之后的getSum.
//3 回调函数(例如定时器),一般使用bind来修改this指向
let obj = {
name:"班长"
};
//3.1 定时器的回调函数是一个具名函数
function test1(){
console.log(this);
}
let test2 = test1.bind(obj);
setInterval(test2,2000);
//3.2 定时器的回调函数是一个匿名函数
setInterval(function () {
console.log ( this );
}.bind(obj),2000);
1.2-函数调用上下文模式注意点
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
/*
1. 函数上下文执行模式 : 动态修改this
注意点 : 修改的this只能是引用类型
2.如果写的是基本数据类型
string,number,boolean : 自定帮我们转成对应的引用类型(基本包装类型) new String() Boolean() Number()
undefined,null :代码不会报错,也不会帮我们修改this,还是原来的this
*/
function fn(){
console.log(this);
};
fn.call('str');
fn.call(1);
fn.call(true);
//如果传的是undefined和null,或者不传。代码不会报错,也不会帮我们修改this,还是原来的window
fn.call(undefined);
fn.call(null);
fn.call();
fn.call(window);
</script>
</body>
</html>
1.3-案例01:伪数组转数组
本小节知识点:伪数组转数组
- 1.伪数组:只有数组的三要素(元素、下标、长度),没有数组的api
- 2.转数组目的:让伪数组也可以调用数组的api
- 3.方式很多种,掌握任何一种即可
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
/*
伪数组 : 拥有数组三要素,但是没有数组的api
伪数组本质是一个对象
*/
let weiArr = {
0: '林绿裙',
1: '保健坤',
2: '泰拳邹',
3: '炮王周',
length: 4
};
console.log(weiArr);
console.log(weiArr[0]);
//需求:伪数组转成真数组 (希望伪数组也可以调用数组的api)
//1.遍历伪数组添加到真数组中
// let arr = [];
// for (let i = 0; i < weiArr.length; i++) {
// console.log(weiArr[i]);
// arr.push(weiArr[i]);
// };
// console.log(arr);
//2. ** arr.push.apply()
/*
arr.push() : 支持多个参数
*/
let arr = [];
//这里使用apply不是为了修改this,而是利用applay传参会自动遍历数组/伪数组每一个元素作为实参传递
arr.push.apply(arr, weiArr);
console.log(arr);
//3. ** slice
/*
(1) 数组arr.slice() 作用是查询数组元素。 如果传0 或者 不传,则会自动返回数组本身
(2) 如果 伪数组 可以调用slice(), 则会自动返回数组本身(真数组)
(3) 但是 伪数组 不能调用slice(), 因为伪数组的原型 不是Array.prototype
(4) 思考: 伪数组如何可以调用slice()?
(5) 突破点: slice() 方法在数组的原型中Array.prototype
原型中的成员:两种对象可以访问
* a.构造函数自身可以访问
* b.每一个实例对象
(6) 越过原型链查找机制,直接访问构造函数原型中的slice() : Array.prototype.slice()
(7) 如果使用构造函数原型来调用slice(), 此时this是谁呢? 变成原型: Array.prototype
(8) 使用 Array.prototype.slice.call(weiArr)
*/
//weiArr.slice();//报错 为什么报错? weiArr的原型不是Array.prototype
//slice方法存储在构造函数原型中,谁可以访问原型?
//1.实例化对象
//arr.sclie();
//2.构造函数自身
//Array.prototype.slice()
//越过原型链,直接从构造函数原型中调用slice方法
let arr = Array.prototype.slice.call(weiArr);
console.log(arr);
/* 手动模拟伪数组可以调用slice()原理 */
//1.有一个构造函数Person (相当于Array)
function Person(name, age) {
this.name = name;
this.age = age;
};
//Person原型添加成员(相当于slice)
Person.prototype.sayHi = function () {
//this : 谁调用我就指向谁 (正常情况下:this指向调用这个方法person实例对象)
console.log(this.name);
};
//2. p1 和 p2都是Person的实例对象, 所以可以访问 Person原型中的sayHi()方法
let p1 = new Person('困困', 20);
p1.sayHi();
let p2 = new Person('琪琪', 30);
p2.sayHi();
//3. 有一个obj(属性名和p1一样,但是原型指向的不是Person,相当于伪数组)
let obj = {
name: '班长',
age: 88
};
//4. obj不能直接调用sayHi,会报错
// obj.sayHi();
//5. 解决方案: 越过原型链,直接访问Person.prototype中的sayHi
// 但是这样调用的话, sayHi中的this就变成了 Person.prototype
// 所以需要使用call,把this修改成obj. 就相当于是obj在调用sayHi()
Person.prototype.sayHi.call(obj);
</script>
</body>
</html>
1.5-案例02:求数组最大值
本小节知识点
- 通过apply方式,让数组对象可以调用Math对象的方法
- 原因:apply方法传参的时候,会自动将数组的每一个元素取出来作为实参
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
let arr = [100,20,66,88,90];
//1.擂台思想
let max = -Infinity;
for(let i = 0;i<arr.length;i++){
if(arr[i] > max){
max = arr[i]
};
};
console.log(max);
//2.Math.max(数字1,数字2,数字3,…………)
// let max = Math.max(arr[0],arr[1],arr[2],arr[3],arr[4]);
//这里没有必要修改this,借助apply可以自动遍历数组传参
let max1 = Math.max.apply(Math,arr);
console.log(max1);
</script>
</body>
</html>
1.6-案例03:万能检测数据类型
- 思考题:为什么数组调用toString和对象调用toString得结果不一样
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
/*
1. 检测数据类型 : typeof 数据
特点:两种数据类型无法检测 null与array
*/
console.log(typeof null);//object
console.log(typeof [10,20,30]);//object
//3. 万能检测数据类型法 :
// Object.prototype.toString.call(数据)
/*
检测数据类型需要使用Object原型(Object.prototype)中的toString()来检测
返回一个固定值: [object 数据类型]
*/
console.log( Object.prototype.toString.call('123'));//[object String]
console.log( Object.prototype.toString.call(666));//[object Number]
console.log( Object.prototype.toString.call(true));//[object Boolean]
console.log( Object.prototype.toString.call(undefined));//[object Undefined]
console.log( Object.prototype.toString.call(null));//[object Null]
console.log( Object.prototype.toString.call([10,20,30]));//[object Array]
console.log( Object.prototype.toString.call(function(){}));//[object Function]
console.log( Object.prototype.toString.call({}));//[object Object]
/*
请说出下列代码的结果,如果不是Null,如何得到Null
*/
console.log( typeof null );
</script>
</body>
</html>
03-递归
1.1-递归函数介绍
本小节知识点
- 1.递归函数:一个函数自己调用自己
- 2.递归函数特点
- a.一定要有结束条件,否则会导致死循环
- b.能用递归函数实现的需求,就一定可以用循环调用函数来解决,只是代码简洁与性能不同而已
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<script>
/*
1. 递归函数 : 在函数中自己调用自己
2. 递归特点
a. 能用递归实现的功能一定可以用循环,只是语法不同
b. 递归一定要有结束的条件,否则会导致死循环
*/
//一个函数递归
// function fn(){
// console.log('哈哈');
// fn();
// };
// fn();
//两个函数递归
// function fn1(){
// console.log('哈哈');
// fn2();
// };
// function fn2(){
// console.log('呵呵');
// fn1();
// };
// fn2();
//需求:写一个函数,打印三次 班长爱坤哥
let i = 1;
function fn(){
console.log('班长爱坤哥');
i++;
if(i <= 3){
fn();
};
//循环实现
// for(let i = 1;i<=3;i++){
// console.log('班长爱坤哥');
// };
};
fn();
</script>
</body>
</html>
1.2-递归应用场景:浅拷贝与深拷贝
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<script>
/*浅拷贝与深拷贝概念主要针对于对象这种数据类型
1.浅拷贝: 拷贝的是地址
* 特点:修改拷贝后的数据,原数据也会随之修改
2.深拷贝:拷贝的是数据
* 特点:修改拷贝后的数据,对原数据没有影响
*/
let obj = {
name: 'ikun',
age: 32,
hobby: ['讲课', '敲代码', '黑马程序员'],
class: {
name: '武汉大前端',
salary: [18888, 12000, 10000]
}
}
//1.浅拷贝: 拷贝的是地址
let obj1 = obj
obj1.name = '黑马李宗盛'
console.log(obj, obj1)
//2.深拷贝:拷贝的是数据
//核心原理:使用递归。 只要遇到属性值是引用类型,则遍历。
function kaobei (newObj, obj) {
// 遍历
for (let key in obj) {
if (obj[key] instanceof Array) {
// obj[key] 是数组
// obj[key]是数组
newObj[key] = []
kaobei(newObj[key], obj[key])
} else if (obj[key] instanceof Object) {
// obj[key] 是对象
// obj[key]再遍历拷贝
newObj[key] = {}
kaobei(newObj[key], obj[key])
} else {
newObj[key] = obj[key]
}
}
}
let obj2 = {}
//深拷贝
kaobei(obj2, obj)
//修改拷贝后的数据
obj2.name = '黑马颜值担当'
obj2.hobby = '唱歌'
console.log(obj,obj2)
</script>
</body>
</html>
1.3-递归应用场景:遍历dom树
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<style>
* {
padding: 0;
margin: 0;
}
.menu p {
width: 100px;
border: 3px solid;
margin: 5px;
}
.menu > div p {
margin-left: 10px;
border-color: red;
}
.menu > div > div p {
margin-left: 20px;
border-color: green;
}
.menu > div > div > div p {
margin-left: 30px;
border-color: yellow;
}
</style>
</head>
<body>
<div class="menu">
<!-- <div>
<p>第一级菜单</p>
<div>
<p>第二级菜单</p>
<div>
<p>第三级菜单</p>
</div>
</div>
</div> -->
</div>
<script>
//服务器返回一个不确定的数据结构,涉及到多重数组嵌套
let arr = [
{
type: '电子产品',
data: [
{
type: '手机',
data: ['iPhone手机', '小米手机', '华为手机']
},
{
type: '平板',
data: ['iPad', '平板小米', '平板华为']
},
{
type: '智能手表',
data: []
}
]
},
{
type: '生活家居',
data: [
{
type: '沙发',
data: ['真皮沙发', '布沙发']
},
{
type: '椅子',
data: ['餐椅', '电脑椅', '办公椅', '休闲椅']
},
{
type: '桌子',
data: ['办公桌']
}
]
},
{
type: '零食',
data: [
{
type: '水果',
data: []
},
{
type: '咖啡',
data: ['雀巢咖啡']
}
]
}
]
/* 使用递归遍历数组 */
function addElement (arr, father) {
for (let i = 0; i < arr.length; i++) {
let div = document.createElement('div')
div.innerHTML = `<p>${arr[i].type || arr[i] }</p>`
father.appendChild(div)
if( arr[i].data ){
addElement(arr[i].data,div)
}
}
}
//调用递归函数
addElement(arr,document.querySelector('.menu'))
</script>
</body>
</html>
04-闭包(closure)
- 传送门:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
学习目标:能够认识闭包结构,知道闭包在开发中的作用。
一. 闭包的定义
- 1.闭包 : 是一个可以访问其他函数作用域的函数
- 闭包 = 函数 + 上下文的引用,闭包不等于函数。以下代码就构成了闭包:
function fn(){
let a = 1
function fn1() {
console.log(a)
}
fn1()
}
提示:执行函数
fn1
用到了另一个函数fn
中的a
这个变量,所以fn1 + a
构成了闭包。
二. 闭包的作用
直接作用:解决变量污染问题,让变量被函数保护起来。
示例代码如下:
let count = 0
setInterval(function () {
console.log(count++)
}, 1000)
以上代码中的 count
是一个使用频率很高的变量名,为了避免和其他位置的代码冲突,可以再使用一个函数把以上代码包裹起来,起到保护作用。
function fn() {
let count = 0
setInterval(function () {
console.log(count++)
}, 1000)
}
以上代码中,setInterval
第一个参数的匿名函数与 count
构成了闭包。
将以上代码改写如下:
function fn() {
let count = 0
function add() {
console.log(count++)
}
setInterval(add, 1000)
}
以上代码中,add + count
构成了闭包。
结论:一个函数内使用了外部的变量,那这个函数和被使用的外部变量一起被称为闭包结构,在实际开发中,通常会再使用一个函数包裹住闭包结构,以起到对变量保护的作用。
三. 闭包的案例
-
案例需求:在输入框输入搜索文字,点击百度一下按钮,用定时器模拟网络请求,1 秒之后显示搜索结果;
-
页面结构如下
<div class="box"> <input type="search" name="" id=""> <button>百度一下</button> </div>
-
代码如下:
// 1. 获取元素 let search = document.querySelector('.box input') let btn = document.querySelector('.box button') // 2. 添加点击事件 btn.onclick = function () { // 获取搜索的文字 let text = search.value // 模拟发送网络请求 setTimeout(function () { alert(`您搜索的内容是 ${text} 共搜索到 12345 条结果`) }, 1000) }
闭包小结
- 闭包 = 函数 + 上下文的引用
- 闭包的作用:解决变量污染问题,让变量被函数保护起来。
- 在 ES5 时代,闭包可以解决一些其他 JavaScript 的小 BUG,但随着 ES6
let
等新语法的诞生,之前一些闭包的使用场景已经不再需要.
- 在 ES5 时代,闭包可以解决一些其他 JavaScript 的小 BUG,但随着 ES6
今天学习重点梳理(高频面试题)
this三种指向
this : 谁 调用
我,我就指向谁
1.全局函数 : this指向window
2.对象方法 : this指向对象
3.构造函数 : this指向new创建的空对象
call、apply、bind区别
- 相同点:都是修改函数this指向
- 不同点
- 传参方式不同: call用于单个参数,apply用于多个参数
- 执行机制不同: call与apply会立即执行, bind不会立即执行
- call、apply用一次修改一次
- bind;一次修改,终生有效
闭包
- 什么是闭包:以下两种回答都可以
- 闭包是一个访问其他函数内部变量的函数
- 闭包是 函数 + 上下文代码组合
- 闭包作用:解决变量污染
递归
- 什么是递归:函数内部调用自己
- 递归场景
- 深拷贝
- 遍历dom树