递归函数
递归函数本质上就是函数内部自调用,这时如果我们不加判断条件,那么函数就会不停地去调用,所以我们要给函数加上判断条件,用于跳出循环。
// 递归 函数内部自调用,但是必须要有跳出循环的条件
function back (n){
if(n==5) {
return 0;
}{
return back(n+1);
}
}
console.log(back(3)); // 0
- 斐波那契数列(兔子数列)
0,1,1,2,3,5,8…从第三个数开始,它的值等于前面两个值相加,这就是斐波那契数列
function fn(n){
if(n===1){
return 0
}else if(n===2){
return 1
}else{
return fn(n-1) +fn(n-2);
}
}
console.log(fn(4));
这种写法,单独判断了第一个数和第二个数的情况,虽然可以获取我们想要的结果,但是运算量比较大,我们还可以尝试以下的写法
function getSum(n,curr=0,next=1){
if(n===1){ // 这个条件用于跳出循环
return curr;
}else{
return getSum(n-1,next,curr+next);
}
}
console.log(getSum(1));
这里输入的n就不是参与运算的参数了,而是充当了一个计数器的角色
- 麦粒案例
这也是一个经典的案例,第一个格子有一粒麦子,第二个格子有2粒麦子,第三个格子有4粒麦子,后面一个的数量是前一个的两倍
// 麦粒
function getMIke(n){
if(n===1){
return 1
}else{
return Math.pow(2,n-1)+getMIke(n-1); // 每一个格子的数量是 2的n次方-1
}
}
console.log(getMIke(2));
箭头函数
-
箭头函数省去了普通函数的function的声明,直接使用了这样的格式
var fun = (参数)=>{代码}
,小括号里放要传入的参数,花括号里写执行的代码。 - 当传入的参数有且只有一个时,小括号可以省略,其他情况都要加上小括号
- 当执行的代码只有一句时,花括号也可以省去
- 当执行的代码只有一句时,return也可以省去
// 箭头函数
var a = ()=>{console.log("这是一个箭头函数");}
a();
// 如果参数有且只有一个
var b = aa=>{console.log("参数只有一个");}
b();
// 如果方法只有一句,{} 可以省略
var c = ()=>console.log("方法只有一句");
c();
// 如果方法只有一句,return也可以省略
var d = () => "你看return也可以省略";
console.log(d());
- 注意:箭头函数是没有this指向的,但是我们可以在箭头函数里使用this,这里的this就是指向它的父级了。
- 我个人认为箭头函数没有this指向的含义指的是,没有this是指向箭头函数,即便里面写了this,也只是指向它的父级而已
// 箭头函数没有this指向,也就是说箭头函数中的this是不能指向它自己的,箭头函数中的this指向的是他的父级,
window.document.getElementById("box").onclick = ()=>console.log(this); // this指向window
// 对象中的函数
var obj = {
name:"这是一个对象",
// fn:function(){console.log(this);} // 指向当前对象
fn:()=>console.log(this) // window
}
obj.fn();
// 构造函数
function fun (name){
this.name=name;
// this.aa = function(){console.log(this);} // 指向实例
this.aa = () => console.log(this); // 目前结果上是指向实例
}
console.log(fun);
var newA = new fun("张三");
newA.aa();
// 构造函数原型上添加方法
// fun.prototype.addFun = function(){console.log(this);} // 指向实例
fun.prototype.addFun = ()=>console.log(this); // 指向window
newA.addFun()
浅拷贝、深拷贝
- 在了解浅拷贝和深拷贝之前,我们再来简单地了解一下栈和堆
- 栈是我们存放一些值类型数据的地方,而堆是我们用来存放引用类型数据的地方。
- 比如说我们声明的一些变量名、字符串之类的就存放在栈里面,而这些变量名代表的对象、方法等是存- 放在堆里的,我们使用变量名的时候,就能够从堆里提取出相应的对象、方法等
浅拷贝
- 浅拷贝是只拷贝一层数据,深层对象就拷贝它的地址
- 浅拷贝之后,我们无论是改变拷贝值或者被拷贝的值,都会让数据同步地发生变化
// 浅拷贝 只拷贝一层 深层对象只拷贝地址:栈里存放变量名,堆里存放引用数据类型 拷贝后的内容修改,原数据也会修改
let obj1 = {
name:"aa",
age:5
}
let obj2 = obj1; // 浅拷贝
console.log(obj1);
console.log(obj2); // 打印的结果都是相同的
obj2.name = "bb";
console.log(obj1);
console.log(obj2);
obj1.name="cc";
console.log(obj1);
console.log(obj2);
深拷贝
- 深拷贝能够拷贝到每一层数据,我们无论修改拷贝前或者拷贝后的数据,都不会对对方产生影响
// 深拷贝 每一层都拷贝,两个内容不会影响
let obj3 = {
name:obj1.name,
age:obj1.age
}
console.log(obj3);
obj3.name = "ee";
console.log(obj3);
console.log(obj1);
我们可以通过上面这种手动的方式来进行深层拷贝,但是在数据很多的时候是行不通的,我们还有其他的办法:
- 使用Object.assgin(),它可以将所有可枚举的属性值,从一个或者多个对象上分配到目标上
// 第一种方法,Object.assgin() 用于将所有的可枚举的属性值,从一个或多个对象分配到目标上
let obj4 = {
name:"dd",
age:15,
time:new Date,
reg:/^\d{10}$/
}
let obj5 = {};
Object.assign(obj5,obj4);
console.log(obj5);
-
使用JSON的方法
但是使用它存在一些弊端,就是对象中使用了date对象时,时间上会存在一些偏差,它是直接复制时间的字符串,而不是去解析代码
// 第二种方法,使用json 转换时间存在问题,将date转换为字符串
let obj6 = JSON.stringify(obj4);
obj6=JSON.parse(obj6);
console.log(obj6);
-
使用递归
这种方法相对来说是很复杂的,需要进行很多的判断。目前这个方法我认为可能还存在一些错误,也希望如果有人发现,可以多多指点,谢谢
// 第三种方法 递归
function deepC (e) {
let a ;
if (typeof e === "object") {
// 判断是否为数组
if(Array.isArray(e)){
a=[];
for(let i in e){
a.push(deepC(e[i]))
}
}else if(e===null){
// 判断是否为null
a=e
}else if(e.constructor===RegExp){
// 判断是否为正则
a=e
}else if(e.constructor === Function){
// 判断是否为函数
a=e
}else{
// 判断是否为对象
a = {};
for(let i in e){
a[i] = deepC(e[i])
}
};
}else{
// 值类型数据直接赋值
a=e;
}
return a;
}
symbol数据类型
它是ES6新增的数据类型,代表独一无二的值,存储关键数据或者服务器数据
// Symbol代表唯一的数据
let a = Symbol("11");
let b = Symbol("11");
console.log(a==b); // false
console.log(a===b); // false 它们都是各自的唯一的值,是不相等的
- description 获取symbol中的值
// 获取symbol中的值 description
console.log(a.description);
console.log(a.description === b.description); // true 这里的比较单纯是在比较两个值是否是相同的
- for 用来指定一个symbol的空间
// for 通过for来指定symbol的空间
let c = Symbol.for("22");
let d = Symbol.for("22");
console.log(c===d); //true 来源相同值也相同,所以返回true
- keyFor 通过变量名,反向查找symbol数据,不过只对于用for声明出来的数据才起作用
// keyFor 通过变量名反向 查找symbol数据,只有用for声明出来的变量才可以使用
console.log(Symbol.keyFor(c)); // 22
console.log(Symbol.keyFor(d)); // 22
Object对象
- defineProperty(obj,prop,descriptor)用于重新定义对象的属性
// Object.defineProperty(obj,prop,description) 重新定义对象
let obj = {
name:"aa",
age:15,
sex:"男"
}
console.log(obj);
Object.defineProperty(obj,"age",{
value:16,
});
console.log(obj);
- delete obj.prop 删除对象的属性
// 删除对象中的属性
delete obj.sex;
console.log(obj);
- writable:true/false 规定定义的属性是否可以修改,默认是true
- enumerable:true/false 规定定义的属性是否可以被遍历出来,默认为false
- configurable:true/false 规定定义的属性是否可以被删除,默认为false
let obj2 = {
key1:1,
key2:2,
key3:3
}
Object.defineProperty(obj2,"key1",{
value:111,
writable:false, // 禁止修改
})
console.log(obj2);
obj2.key1 = 3333;
console.log(obj2); // 没有修改
Object.defineProperty(obj2,"key2",{
value:222,
enumerable:false, // 禁止遍历
})
for(let i in obj2){
console.log(obj2[i]);
}
Object.defineProperty(obj2,"key3",{
value:333,
configurable:false, // 禁止删除
})
delete obj2.key3;
console.log(obj2); // 不可删除
delete obj2.key2;
console.log(obj2); // 可删除
Promise
ES6新增的构造函数,创造一个promise实例对象,代表将来要发生的事情,用来传递异步操作
Promise对象有3个状态
- pending 初始状态
- fulfilled 成功状态
- reject 失败状态
我们创建Promise对象时,promise构造函数是可以接收一个函数作为参数的,而这个函数有两个参数:resolve和reject。这两个参数是js原先已经设置好的函数,有着各自的作用。
- resolve可以将初始状态变为成功状态,并能将一个值抛出去
- reject可以将初始状态变为失败状态,并将一个值抛出去
// Promise是构造函数,创造一个promise实例对象,代表将来要发生的事情,用来传递异步函数
const pro = new Promise(function (resolve, reject) {
let a = 0; // 初始状态
// console.log(a);
if (true) {
// 成功的状态
a = 1;
resolve(1)
} else {
// 失败的状态
a = 2;
reject(2)
}
})
console.log(pro);
-
then()、catch()
then()是当成功时执行的代码
catch()是当失败时执行的代码
它们是promise原型链上的方法,可以进行连写,后面的回调函数可以使用上一个回调函数返回的结果
// 链式编程,后面的回调函数可以使用上次返回的结果
// then是成功时进行的操作,catch是失败时进行的操作
pro.then(r=>{
console.log(r);
return r*3
}).then(r=>console.log(r)).then(r=>console.log(r))
下面的案例用到了then和catch
我们从下面的案例中可以看到,当运行到了reject的时候,下面就直接进行了catch里的代码,说明当reject执行时,就将状态转变为了失败的状态,便去执行失败时的代码(catch的代码)
pro.then(r => {
console.log(r);
return r * 4
}).then(r => {
console.log(r);
return Promise.resolve(r);
}).then(r => {
console.log(r);
return Promise.reject(r) // reject表示失败时传的值
}).then(r => // 由于之前是失败时抛出的状态,所以不会走这个流程
{ console.log(r) }
).catch(e => console.log(e))
回调地狱
回调函数里嵌套回调函数就叫回调地狱,可以看一下下面的案例
// 地狱回调
setTimeout(() => {
console.log("这是第一句");
setTimeout(() => {
console.log("这是第二句");
setTimeout(() => {
console.log("这是第三句");
setTimeout(() => {
console.log("这是最后一句");
}, 1000);
}, 1000);
}, 1000);
}, 1000);
这样做会导致我们的代码可读性很差,后期不好维护,我们可以用Promise来解决这个问题
// 以promise的形式进行
function tryF(str) {
var time = new Promise(function (r, e) {
setTimeout(() => {
if (true) r(str) // 操作成功时将str抛出去
else e("操作失败") // 操作失败时返回
}, 500);
})
return time
};
tryF("这是第一句").then(r => {
console.log(r); // 需要把r打印出来,否则是看不到结果的
return tryF("这是第二句")
}).then(r => {
console.log(r);
return tryF("这是最后一句");
}).then(r => {
console.log(r); // 这一步也是必须的,否则最后一句话是打印不出来的
})
我们还可以使用Promise封装一个ajax函数:
// promise封装ajax
function getData(url, type, data) {
let pro = new Promise(function (r, e) {
// 初始化
let xml = new XMLHttpRequest;
// 判断状态
xml.onreadystatechange = function () {
if (this.readyState != 4) {
return; // 如果准备状态不是4,那就不进行后面的操作
}
if (this.status === 200) {
// 成功时
r(this.response)
} else {
// 失败时
e(new Error(this.statusText))
}
}
// 设置请求
xml.open(type, url);
xml.responseType = "json" // 返回的数据格式
xml.setRequsetHeader("Accept", "application/json"); // 设置请求头
xml.send(data); // 发送请求
})
return pro; // 返回数据
}
总结
今天的学习,难点主要在Promise。并不太理解它的用法和机制,尤其是在利用它解决回调地狱的问题和封装ajax的问题上,不过我认为,这种问题必须要有实际的案例,只有运用上了才能很好地解决掉自己的问题,否则看再多的资料,也不能彻底地打消心里的疑惑,所以现在的策略就是背下来就好。