01 预解析
01-代码段
<!-- 可以在下面的代码段中使用上面的代码段中的数据 -->
<!-- 1个script标签就是一个代码段 -->
< script>
// 在一个代码段中就可以写JS代码
var a = 110;
function fn(){
console.log("fn...");
}
// 上面代码段不能使用下面的代码段中定义的数据
console.log(ok); // Uncaught ReferenceError: ok is not defined
// function (){} // 语法错误,未写函数名
</script>
<!-- 上面代码段报错会影响下面的代码段 -->
< script>
var b = 220;
// 可以在下面的代码段中使用上面的代码段中的数据
console.log(a);
fn();
var ok = true;
JS代码的执行分两个阶段:一个叫预解析,一个叫执行,预解析结束后,才会进入到执行阶段。
02-什么是预解析
什么是预解析?
-
浏览器在执行JS代码的时候会分成两部分操作:预解析以及逐行执行代码
-
也就是说浏览器不会直接执行代码, 而是加工处理之后再执行,
-
这个加工处理的过程, 我们就称之为预解析 预编译期间做了什么?
-
把声明提升:加var的变量就是被提升,function声明的函数也会提升,提升到代码段的最前面。
-
函数内部的局部变量,提升到函数体的最前面。
-
注意:变量的提升仅仅是声明; 函数的提升不仅提升了声明,也提升赋值 函数内部才是局部变量,没有加var的变量都是全局变量
console.log(a); // 结果为undefine 因为var a提升到代码段的最前面,未赋值 var a = 110; // var a是声明,把110赋值给a console.log(a); // 预解析会声明: // 1.声明变量 var a; 2.声明函数 function fn()
练习题:
02 执行上下文
01-代码执行流程
内存分区:
- 我们只需要掌握两个区,一个是栈区,一个是堆区。
- 基本数据类型,是存储在栈区的,引用数据类型是存储在堆区,堆区的地址,还是保存在栈区
JS代码分两类:
- 全局代码:函数外面的代码都是全局代码
- 函数代码:一个函数就是一个局部代码
分析如下:
- 全局代码执行产生ECG(Execution Context Gloable),每当调用一个函数,就产生一个函数的EC。每产生一个EC,需要放到ECS(Execution Context Stack),当函数调用完毕,这个EC就是出栈,出栈后的EC,会被销毁,所谓的销毁指是它们分配的内存空间都要被释放掉。
总结:
- 当全局代码执行时,就会产生一个全局的执行上下文,EC(G);
- 当函数代码执行时,就会产生一个局部的执行上下文,EC(Fn)。只要调用一个函数,就会产生一个局部执行上下文。调用100个函数,就会产生100个执行上下文。
02-GO)
js引擎会在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
-
该对象 所有的作用域(scope)都可以访问;
-
里面会包含Date、Array、String、Number、setTimeout、setInterval等等;
-
其中还有一个window属性指向自己;说白了,GO就是window
-
只要我们写的全局变量或在全局中写的函数,都会挂载到window上面
<script> console.log(window); // GO就是window var a = 110; function fn(){ console.log("fn..."); } fn() // window可以不写 window.alert("xxx");
<script> var a = 110; function fn(){ console.log("fn..."); } console.log(window.a); window.fn();
<script> // 练习 var n = 110; console.log(n); // 110 console.log(window.n) // 110 全局变量挂在window上 m = 220; // 没有加var也是全局变量 console.log(m); // 220 console.log(window.m); // 220 console.log(window.name); // 空串(window里有name) console.log(window.xxx); // undefine 访问一个对象上不存在的属性结果是undefine
执行上下文理解
ECG被放入到ECS中里面包含两部分内容:
- 第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值;这个过程也称之为变量的作用域提升(hoisting)
- 第二部分:在代码执行中,对变量赋值,或者执行其他的函数;
ECF中包含三部分内容:
- 第一部分:在解析函数成为AST树结构时,会创建一个Activation Object(AO): AO中包含形参、arguments、函数定义和指向函数对象、定义的变量;
- 第二部分:作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找;
- 第三部分:this绑定的值:这个我们后续会详细解析;
面试题
<script>
var a = 1;
var a = 2;
var a = 3;
console.log(a); // 3
</script>
<script>
var a = 1;
function a(){
console.log("a...");
}
console.log(a); // 1
</script>
<script>
var a = 10;
var b = a; // 把a对应栈空间中的数据赋值给b
console.log(a,b); // 10 10
b = 1;
console.log(a,b); // 10 1
</script>
<script>
var a = [1,2];
var b = a; // 把a对应的栈空间中的地址copy一份,给b
console.log(a,b); // [1,2] [1,2]
b = [3,4];
console.log(a,b); // [1,2] [3,4]
</script>
<script>
var a = [1,2];
var b = a;
console.log(a,b); // [1,2] [1,2]
b[0] = 110;
console.log(a,b); // [110,2] [110,2]
</script>
<script>
var a = [1,2];
var b = [1,2];
// 比较的是栈区中的数据 地址
console.log( a == b ); // false 地址不一样(等于)
console.log( a === b ); // false(绝对等于)
</script>
<script>
var a = 110;
var b = 110;
console.log(a==b); // false
</script>
<script>
var a = [1,2];
var b = a;
console.log(a == b); // ture
console.log(a === b); // ture
</script>
<script>
console.log(a,b); // und und
var a = b = 110; // b is not defined
// var a = b = 1; 等价于 var a = 1; b = 1; 没有var不提升
</script>
<script>
var a = {m:666}; // 表示对象
var b = a; // b = {m:666};
b = {m:888};
console.log(a.m); // 666
</script>
<script>
var a = {n:12};
var b = a;
b.n = 13; // 13赋值给n
console.log(a.n); // 13
</script>
<script>
var m = 1;
n = 2;
// 都是全局变量 全局变量要提升
console.log(window.m); // 1
console.log(window.n); // 2
</script>
<script>
function fn(){
var a = 111; // a为局部变量 不可以放在GO(window)上
}
fn();
console.log(window.a); // 访问一个不存在的变量 结果为undefine
</script>
<script>
var a = -1;
if(++a){ // 0本身是false
console.log("666");
}else{ // false
console.log("888");
}
// 打印出888
</script>
<script>
console.log(a,b); // und und(不管怎样a,b先提升)
if(true){
var a = 1; // 给a赋值
}else{
var b = 2;
}
console.log(a,b); // 1 und
</script>
<script>
var obj = {
name:"wc",
age:18
}
// in是一个运算符 用来判断某个属性是否属于某个对象
console.log("name" in obj); // true
console.log("age" in obj); // t
console.log("address" in obj); // f
</script>
<script>
var a;
console.log(a); // und
if("a" in window){
a = 110;
}
console.log(a); // 110
</script> -->
<script>
console.log(a); // undefine
if("a" in window){
var a = 110;
}
console.log(a); // 110
<script>
var a = 110;
function fn(){
console.log(a); // und
return;
var a = 220;
console.log(a); // xxx
}
fn();
console.log(a); // 110
<script>
var n = 110;
function foo(){
n = 220; // 没加var是全局变量
}
foo();
console.log(n); // 220
</script>
<script>
function foo(){
var a = b = 110; // 等价于 var a = 110;b = 110;
}
foo();
console.log(a); // a是局部变量无法访问 报错
console.log(b);
</script>
03-堆栈内存
<script>
var a = 1;
var b = "hello";
function fn(){
console.log("fn...");
}
var arr = ["a","b","c"];
var obj = {name:"wc",age:100}
</script>
04-作用域链
ECF中包含三部分内容:
- 第一部分:在解析函数成为AST树结构时,会创建一个Activation Object(AO): AO中包含形参、arguments、函数定义和指向函数对象、定义的变量;
- 第二部分:作用域链:由VO(在函数中就是AO对象)和父级VO组成,查找时会一层层查找;
- 第三部分:this绑定的值:这个我们后续会详细解析;
练习题(面试题)
<script>
function fn(a){
console.log(a); // 110
}
fn(110)
console.log(a); // 报错
</script>
<script>
var arr = [11, 22];
function fn(arr) {
arr[0] = 100;
arr = [666];
arr[0] = 0;
console.log(arr); // [100,22]
}
fn(arr);
console.log(arr); // [0]
</script>
<script>
var a = 1; // 全局变量
var b = 1; // 全局变量
function gn(){
console.log(a,b); // (undfine,1)
var a = b = 2; // 等价于var a = 2;b = 2; a局部变量,b全局变量
console.log(a,b); // (2,2)
}
gn();
console.log(a,b); // (1,2) 函数调用时只能访问全局变量
</script>
<script>
var a = 1;
var obj = { uname:"wangcai" }
function fn(){
var a2 = a;
obj2 = obj; // 多个变量,相当于 var obj2 = obj;var a2 = a;
a2 = a;
obj.uname = "xiaoqiang";
console.log(a2); // 1
console.log(obj2); // "xiaoqiang"
}
fn();
console.log(a); // 1
console.log(obj); // "xiaoqiang"
</script>
<script>
var n = 100;
// 找一个函数的父的EC,看的是函数的定义处,不是调用处
function fn(){
console.log(n);
}
function gn(){
var n = 200;
console.log(n);
fn()
}
gn()
console.log(n);
</script>
03 深入变量与闭包
01-加var的变量和不加var的变量
加var的变量和不加var的变量有什么区别:
-
加var的变量在预编译期间会提升,不加var的变量在预编译期间不会提升。
console.log(a); // undfine var a = 110; console.log(b); // 报错 b = 120;
-
不管有没有加var,创建的全局变量,都会放到GO中的,也就是可以通过window.xx。
var a = 110; b = 120; console.log(window.a); console.log(window.b);
-
加var的变量,可能是全局变量,也可能是局部变量,不加var的变量,只能是全局变量。
-
加var的局部变量,不会作用window的属性
function fn(){ var a = 110; } fn(); console.log(window.a);
不加var创建的变量:
- 不建议使用。 项目中尽量不要使用全局变量。在项目中,不要使用没有加var的变量。
02-let声明变量
使用let声明的变量的特点:
-
使用let声明的变量没有提升。针对这个错误:ReferenceError: Cannot access 'a' before initialization有人是这样说的:let声明的变量也有提升,但是没有赋值(没有初始化)。记住:let也可以提升只不过提升后报错,赋值后才有值;而var提升后为undefine,赋值后有值
<script> console.log(a); let a = 110; </script> ES6之前只有两种作用域: 1-全局作用域 2-局部作用域 在其它编程语言中,还有块级作用域:{} <script> var a = 110; // a是处于全局作用域名 function fn(){ var b = 220; // b是处于局部作用域 console.log(b); } fn(); if(true){ // 在其它编程语言中,{}就是一个块级作用域 // 在块级作用域中定义的变量,只能在块中使用 var b = 111; } console.log(b); // 111 </script>
-
使用let配合{ }会形成块级作用域。 在C语言中,就有块级作用域。在JS中,原本是没有块级作用域。在块中声明的变量,只能在块中使用。
<script> // let和{}就会形成一个块级作用域 // 块级用域域中定义的变量,只能在块中使用 if (true) { let b = 111; } console.log(b); // ReferenceError: b is not defined </script> <script> { var a = 110; let b = 220; } console.log(a); // 110 console.log(b); // b后{}形成块级作用域 </script> <script> for(let i=1; i<=10; i++){ console.log(i); // 形成10个块级作用域 } console.log(i); // 报错 </script>
-
使用let声明的变量并不会挂载到GO中
let a = 10; console.log(window.a); // undfine 访问一个对象上不存在的属性
-
使用let声明的变量不能重复声明
<script> // let不能重复声明 let a = 1; let a = 2; let a = 3; console.log(a); // 报错 </script>
使用var可以重复声明
<script>
var a = 1;
var a = 2;
var a = 3;
console.log(a); // 3
</script>
需要知道:项目中不要使用var来声明变量,你要声明变量,使用let。 使用var声明变量不足:
- 提升
- 重复声明 浏览器中有这样一个机制,如果一个变量提升过了,后面遇到同名变量,就不会提升了。
03-使用const声明常量
<script>
// 变量
let score = 80;
score = 90;
score = 100;
</script>
<script>
// 常量
const PI = 3.14;
console.log(PI); // 3.14
PI = 666;
console.log(PI); // 报错
</script>
<script>
// 声明的常量必须赋值 不能先声明,后赋值
const PI;
PI = 3.14;
</script>
<script>
// let声明变量的特点,const都有
// 1)不会提升
// 2)和{}会形成块级作用域
// 3)不会挂载到GO(window)上
</script>
总结:项目中不要使用var,如果声明变量使用let,如果声明常量,使用const。
04-练习题(面试题)
<script>
// 画图
var a = 12; b = 13; c = 14;
function fn(a){ // a是形参 下面的fn(10)表示把10赋值给a
console.log(a,b,c); // 10 13 14
a = 100; // 没加var 不提升 为全局变量
b = 200; // 没加var 为全局变量
console.log(a,b,c); // 100 200 14
}
b = fn(10); // fn(10)没有返回值 为undfine
console.log(a,b,c); // 12 undfine 14
</script>
05-引出闭包
<script>
// 画图
var i=0;
function A(){ // A是堆
var i=10;
function x(){ // x是堆
console.log(i);
}
return x; // 返回地址
}
var y = A(); // 返回到这里
y(); // 是x() 10
function B(){ // B是堆
var i=20;
y(); // 10
}
B();
</script>
06-闭包练习题
<script>
var i = 5;
function fn(i){
return function(n){
console.log(n+(++i))
}
}
var f = fn(1);
fn(2);
fn(3)(4); // 8
fn(5)(6); //12
f(7); // 9
console.log(i); // 5
</script>
07-高阶函数
在JavaScript中,函数是非常重要的,并且是一等公民:
- 那么就意味着函数的使用是非常灵活的;
- 函数可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用;
什么是高阶函数:
- 一个函数它的参数是函数或它的返回值是函数,那么这个函数就是高阶函数。
- 自己编写高阶函数
<script>
function add(num1,num2){ return num1+num2; }
function sub(num1,num2){ return num1-num2; }
function mul(num1,num2){ return num1*num2; }
// 封装一个计算器函数
// calc是计算器
function calc(num1,num2,fn){ // calc是高阶函数 因为参数fn是函数
console.log(fn(num1,num2)); // fn(num1,num2) 调用函数并返回num1,num2
}
calc(10,20,add); // 加 30
calc(10,20,sub); // 减 -10
calc(10,20,mul); // 乘 200
</script>
<!-- <script>
function fn(){
function gn(){
console.log("gn...");
}
return gn;
}
fn()(); // fn()是个地址(gn函数)、返回值 可以加小括号 即gn() 打印出gn...
let kn = fn(); // 相当于把fn()赋值给kn
kn(); // 然后调用 也可以打印出gn...
</script> -->
- 使用内置的高阶函数
也可以使用map
<script>
// ---------------------- forEach 遍历
let nums = [10, 3, 12, 50, 99, 30];
nums.forEach(function(item){
console.log(item);
})
</script>
<script>
// ---------------------- find 查找
let nums = [10, 3, 12, 50, 99, 30];
var item = nums.find(function(item){
return item === 30
})
console.log(item);
</script>
<script>
// ---------------------- findIndex
let nums = [10, 3, 12, 50, 99, 30];
// 返回找到元素的索引
var item = nums.findIndex(function(item){
return item === 30
})
console.log(item);
</script>
<script>
// ---------------------- find 查找
let names = [
{name:"wc",age:10},
{name:"xq",age:11},
{name:"z3",age:12},
{name:"L4",age:13},
];
var res = names.find(function(item){
// item表示数组中的每一项,是里面的对象
return item.name === 'z3'
})
console.log(res);
</script>
08-什么是闭包
什么是闭包:
-
函数执行后返回结果是一个内部函数,并被外部变量所引用,如果内部函数持有被执行函数作用域的变量,即形成了闭包。
<script> var i = 20; function fn(){ i -= 2; // 这里的i是局部变量 是函数里的i undfine - 2 = NaN var i = 10; // var i提升到函数最前面 return function(n){ console.log((++i)-n); } } var f = fn(); f(1); // 10 11-1 i为10 ++i为11 f(2); // 10 12-2 i为11 ++i为12 // fn()(3)另一种方式 和 var f = fn();f(3)一个道理 fn()(3); // 8 11-3 i为10 ++i为11 console.log(i); // 20 找ECG中的i </script> <script> let a = 0; // a不在GO(window)里 但是全局变量 不提升 b = 0; // b在GO里 不提升 function A(a){ A = function(b){ alert(a+b++); // b的值为3,b++为2 } alert(a++) // a的值为2,a++为1 } A(1); // 1 调用函数产生执行上下文 即产生堆,入栈出栈a=1 A(2); // 4 是函数里面的A 即b=2 </script> <script> var n = 0; function a(){ var n = 10; function b(){ n++; console.log(n); // n的值为11,n++为10 第一次b()打印出11,第二次c()打印出12 } b(); // 11 return b; } var c = a(); c(); // 12 console.log(n); // 0 </script>
09-IIFE
Immediately Invoked Function Expression(立即调用函数表达式)
<script>
function fn(){
console.log("fn...");
}
// 调用一个函数
fn();
// 能不能在声明函数的同时,直接去调用呢?
// 答:IIFE
</script>
-
正确的IIFE,如下:
<script> // IIFE // 写法一: (function fn(){ console.log("fn..."); })() </script> <script> // IIFE // 写法二: (function fn(){ console.log("fn..."); }()) </script> <script> // IIFE // 写法三: +function fn(){ console.log("fn..."); }() </script> <script> // IIFE // 写法四: -function fn(){ console.log("fn..."); }() </script> <script> // IIFE // 写法五: !function fn(){ console.log("fn..."); }() </script>
-
注意问题
-
为了防止,你前面的代码没有加分号,通常写一个IIFE,都会在前面加上分号,如下:
<script>
let obj = {
name:"wc",
age:100
}
// 在写IIFE时,直接以分号开头
;(function(){
console.log("mn...");
})();
</script>
04 深入this
01-this初始
<script>
function fn(){
// 直接写一个this,谁也不知道这个this代表什么
console.log(this);
}
// fn() // this表示window
// var obj = {name:"wc"}
// fn.call(obj) // this表示obj this不确定
var obj2 = {age:18}
fn.call(obj2); // this又表示obj2
// 谁也说不清,现在的this是谁
// 只有代码运行起来后,JS底层才会给this赋值
// this最终值和你书写的位置没有关系
// 和你如何调用fn函数是有关系,不同的调用方式,会给this赋不同的值
</script>
02-默认绑定
独立函数调用就是所谓的默认绑定,独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用
结论:独立的函数调用(即孤零零的函数名加小括号),在浏览器中this表示window
// 独立的函数调用(孤零零的调用函数)
// 如果是独立的函数调用,在浏览器中this表示window
fn();
</script>
<script>
function fn(){
console.log(this); // window
}
function gn(){
console.log(this); // window
fn(); // 独立的函数调用
}
function kn(){
console.log(this); // window
gn(); // 独立的函数调用
}
kn(); // 独立的函数调用 (只要是孤零零的函数名+()都是独立的函数)
</script>
<script>
var obj = {
name:"wc",
fn:function(){
console.log(this);
}
};
var gn = obj.fn;
gn(); // 独立的函数调用
</script>
<script>
function fn(){
console.log(this);
}
var obj = {name:"wc",fn:fn};
var gn = obj.fn;
gn(); // 独立的函数调用
</script>
<script>
function fn(){
function gn(){
console.log(this);
}
return gn;
}
var kn = fn(); //返回的地址
kn(); // 独立的函数调用
</script>
03-隐式绑定
结论:点前面是谁this就是谁
<script>
function fn(){
console.log(this);
}
var obj = {
name:"wc",
fn:fn
};
// 隐式绑定
obj.fn(); // fn中的this是谁呢?答:就看点前面是谁,this表示obj
</script>
<script>
var obj = {
name: "wc",
running: function() {
console.log(this.name + "在跑步...");
},
coding: function() {
console.log(this.name + "在打代码...");
}
}
obj.running(); // this.name相当于obj.name 结果为:wc在跑步
obj.coding(); // this.name相当于obj.name 结果为:wc在打代码
</script>
<script>
var obj = {
name: "wc",
fn: function () {
console.log(this);
} // fn指向这个堆
}
var obj2 = {
name: "xq",
gn: obj.fn // gn指向fn对应的堆
}
obj2.gn(); // this表示obj2 因为点前面是obj2
</script>
04-显示绑定
<script>
// 函数是引用数据类型 引用数据类型都是对象
// 对象是属性的无序集合 可以操作属性 CRUD
// JS中的函数有多种角色:
// 1)普通函数
// 2)方法
// 3)对象
function fn(){
console.log("fn...");
}
fn.a = 1;
fn.b = 2;
// 在JS中一切都是对象
console.log(fn.a); // 1
console.log(fn.b); // 2
</script>
<script>
// 这个fn对象身上默认就有很多的属性和方法
function fn(){
console.log("fn...");
}
// 打印一个对象,可以通过console.dir打印
// console.log(fn)
console.dir(fn) // 打印出的是对象 折叠
</script>
<script>
// 函数对象身上,默认就有很多的属性和方法,
// 我们先学习三个方法
// call apply bind
function fn(){
console.log("fn...");
console.log(this);
}
// call做了什么?
// 1)让fn函数执行
fn.call(); // 打印出fn... 相当于调用函数fn()
// 2)改变函数中this的指向(让call指向函数中的第一个参数obj)
let obj = {name:"wc"}
fn.call(obj); // 让函数中的this指向obj 显示绑定
</script>
-
call
<script> function fn(a,b){ console.log("fn..."); console.log(this); console.log(a+b); } let obj = {name:"wc"} // 第2个参数开始,传递的都是实参(把1传递给a,把2传递给b) fn.call(obj,1,2); // 打印出3 之前是fn(1,2)给ab赋值 </script>
-
apply
<script> function fn(a,b){ console.log("fn..."); console.log(this); console.log(a+b); } let obj = {name:"wc"} // 第2个参数开始,传递的都是实参 fn.apply(obj,[1,2]); // apply与call的唯一区别:apply传递参数必须把参数放在数组中,即加中括号 </script>
-
bind
<script> function fn(a,b){ console.log("fn..."); console.log(this); console.log(a+b); } let obj = {name:"wc"} // bind的作用 // 1)只会改变函数中this指向,不会让函数执行 // 2)返回一个改变了this指向的新函数 let newFn = fn.bind(obj,1,2) newFn(); </script>
总结
<script>
// 显示绑定
// 1)call 2)apply 3)bind
</script>
<script>
function fn(){
console.log("fn...");
console.log(this);
}
fn.call(true)
fn.apply("hello")
let newFn = fn.bind("hello")
newFn();
</script>
05-new绑定
06-内置函数中的this绑定
01-定时器中的this
一次性定时器
<script>
// 2000代表2000毫秒 2s
// setTimeout也是高阶函数 // setTimeout也是window上的,window可以不用写
// setTimeout(function(){
console.log("heihei");
// },2000) // 需等两秒弹出heihei
// 循环定时器
setInterval(function () {
console.log("xixi");
}, 2000) // 每隔两秒都会打印出xixi
</script>
循环定时器
<script>
setInterval(function () {
console.log("xixi");
console.log(this); // 定时器中的this表示window
}, 2000)
</script>
02-事件绑定(监听器)中的this
<body>
<button id="btn">点我</button> // 事件源
<script>
let btn = document.getElementById("btn");
// btn是事件源 click是事件类型(点击事件) function(){}是监听器(事件处理程序)
// 意思是当我点击按钮就会出现函数,点一次出现一次
btn.onclick = function(){
console.log("lala");
// 监听器中的this表示事件源
console.log(this); // 打印出 <button id="btn">点我</button>
}
</script>
</body>
03-数组方法中的this
<body>
<script>
let names = ["wc","xq","z3"];
names.forEach(function(item){ // foeEach表示元素
console.log(item);
console.log(this); // this表示window
})
</script>
<script>
let names = ["wc","xq","z3"];
names.forEach(function(item){
console.log(item);
console.log(this); // this表示{name:"6666"}
},{name:"6666"}) // 传参数
</script>
</body>
07-优先级
- 显示绑定的优先级高于隐式绑定
- new绑定的优先级高于隐式绑定
- new绑定的优先级高于显示绑定(bind)
08-特殊情况
- 参数为undefine、null时,this指向window
- 间接函数引用中,this指向window
09-箭头函数和箭头函数中的this
-
箭头函数
把function替换成=> 1)可以传参数 2)有返回值 简写: 1)形参只有一个时,()可以不写 2)没有形参或有多个,()必须写 3)函数体中只有一条语句时,{}和return都可以不写
-
箭头函数中的this
1)箭头函数中的this需要向外找一级 2)定时器函数里箭头函数中的this指向window 补充:IIFE中的this也指向window
10-断点调试
第一个按钮表示:跳到下一个断点
第二个按钮表示:执行下一步代码
第三个按钮表示:进入函数
第四个按钮表示:出函数
watch监控某个数据、closure闭包、breakpoints断点
05 OOP与原型链
01-创建对象的两种方式
- 第一种方式:加方法
- 第二种方式:字面量形式
02-精细化设置对象的中的属性
01-数据描述符
let newObj = Object.defineProperty(obj,"age",{
value:100, // 给age属性赋值
writable:true, // 表示属性可以修改,默认值是false
enumerable:true, // 表示属性可以遍历,默认值是false
configurable:true, // 表示属性是否可以配置(删除),默认值是false
})
02-存取描述符
-
[[get]]:获取属性时会执行的函数。默认为undefined
-
[[set]]:设置属性时会执行的函数。默认为undefined
let obj = { _age: 18, // 不想让别人访问 } Object.defineProperties(obj, { age: { configurable: true, enumerable: true, get: function () { return this._age; }, set: function (value) { this._age = value; // 设置age时把value赋值给_age:18 } } });
03-同时定义多个属性
definePropertys
04-获取描述符
- getOwnPropertyDescriptor 获取一个描述符
- getOwnPropertyDescriptors 获取多个描述符
03-创建对象的几大方案
01-字面量形式创建对象
缺点:需要写大量的重复代码
let obj1 = {
color:"red", // 属性
start:function(){ // 方法
console.log("start...");
}
};
02-通过new object创建对象
// 不足:不方便批量地创建对象
let coder = new Object();
coder.color = "white"
coder.start = function(){
console.log("开始打代码~");
}
03-通过工厂函数创建对象
工厂函数就用来产生对象
缺点: wc xq z3都是对象 那它的类型是什么? 都是Object 我们希望不同的对象有不同的类型
function createPerson(name,age,height,address){
let p = {}
p.name = name;
p.age = age;
p.height = height;
p.address = address;
return p;
}
let wc = createPerson("wangcai",13,160,"bj"); // 返回值
let xq = createPerson("xiaoqiang",15,180,"sh");
console.log(wc);
console.log(xq);
console.log(wc instanceof Object); // ture
04-通过构造器创建函数
-
通过构造函数创建出来的对象有什么缺点?造成内存空间浪费
-
new做了什么?
1)在构造器内部创建一个对象 2)xxxx 3)构造器中的this指向这个对象 4)执行构造器中的代码 5)返回这个对象
代码如下:
// wc xq z3是对象 对应的类是Person
function Person(name,age,height,address){
this.name = name;
this.age = age;
this.height = height;
this.address = address;
this.running = function(){ console.log(this.name + " is running..."); }
}
// this指向的对象和wc指向的对象是同一个对象
let wc = new Person("wangcai",13,160,"bj")
let xq = new Person("xiaoqiang",15,180,"sh")
let z3 = new Person("zhangsan",20,190,"sz")
wc.running(); // 调方法
xq.running();
z3.running();
function Animal(name){
this.name = name;
}
// pig是对象 对应的类是Animal
let pig = new Animal("pig")
- 有没有更加优雅的解决办法答:有,原型和原型链
05-通过构造函数原型链创建对象
// 构造函数 + 原型对象 创建对象
function Coder(color){
this.color = color;
}
Coder.prototype.start = function(){ // 把函数放在原型对象上
console.log("start...");
}
let obj1 = new Coder("red"); // obj1的私有属性
let obj2 = new Coder("blue"); // obj2的私有属性
04-原型与原型链
01-公有属性和私有属性
只要是一个对象,它身上肯定有一个叫_ _ proto _ _属性,_ _ proto _ _是属性名,它对应的值又是一个对象,_ _ proto _ _叫隐式原型。
-
自己身上的是私有属性
-
_ _ proto _ _ 上的是公有属性
-
先私有后公有,找不到undefine
01 判断一个属性是否是私有属性
hasOwnProperty 判断一个属性是否是私有属性
02 隐式原型
-
每个对象都有一个叫_ _ proto _ _的属性
let arr = ["wc","xq"]; console.dir(arr); arr.abc,找abc 先在自己的私有属性中找,找不到 沿着_ _proto_ _去它的公有属性中找,找不到 再沿着_ _proto_ _去它的公有属性的公有属性中找,找不到 ...... 直到找到_ _proto_ _: null,如果还找不到,就是und console.log(arr.abc);;
03 显示原型
- 每一个函数上都有一个prototype属性,叫显示原型
02-原型链
- 原型链:去对象上找某个属性的查找机制
- 原型对象也是对象,身上也有_ _ proto _ _
- 函数也是对象,身上有_ _ proto _ _
- 找属性都是沿着隐式原型找,即沿着_ _ proto _ _
- undefine + undefine = not a number
原型链练习题(画图)
03-静态属性
-
对象上的属性,叫实例属性 对象就是实例
-
类上的属性,叫静态属性
function Person(name){ this.name = name; Person.total = 110; } let wc = new Person("wangcai"); console.log(wc.name); // name叫实例属性 let xq = new Person("xiaoqiang"); console.log(xq.name); // name叫实例属性 // total是类上的属性,叫静态属性 console.log(Person.total);
05-ES6
01-let声明对象的特点
-
let和 i 会形成块级作用域,出了块级作用域,i 就访问不到了
-
let和{ }也会形成块级作用域,出了块级作用域访问不了
-
在ES5中,是没有块级作用域的,只有全局作用域和局部(函数)作用域
-
同步代码:代码的书写顺序和代码的执行顺序是一样的
-
异步代码:代码的书写顺序和代码的执行顺序不一致————定时器setTimeout
见过的绝大部分代码都是同步代码 console.log(1); // 同步代码 // 定时器是异步代码 setTimeout(()=>{ console.log("hello"); },2000) console.log(2); // 同步代码
-
JS代码执行时:先执行同步代码,再执行异步代码
let的特点:
1)不存在变量提升
2)不能重复定义
3)和{}会形成块级作用域
4)暂存性死区————声明变量之前去使用变量
暂存性死区如下:
var num = 123;
if(true){
num = 666; // 报错 不能在num之前赋值
let num;
}
02-const声明常量的特点
const声明的常量特点:
1)不能提升
2)不能修改值
3)形成块级作用域
4)在声明的同时,必须赋值(不能先声明后赋值)
5)也存在暂存性死区
6)不能重复声明
03-解构赋值
01-数组的解构赋值
-
先解构 后赋值
-
保证赋值运算符两侧的数据类型要一样
let arr = [{ userName:"wc", age:18 },[1,2],110] let [{userName,age},[n1,n2],n3] = arr; // 两侧的数据类型一样 console.log(userName); console.log(age); console.log(n1); console.log(n2); console.log(n3); 或let [obj,[n1,n2],n3] = arr; // 可以把对象换成obj console.log(obj); console.log(n1); console.log(n2); console.log(n3); 没有赋值 undefine let [n1,n2] = [110] console.log(n1); // 110 console.log(n2); // undefine let [n1,n2] = [110,120,130] console.log(n1); // 110 console.log(n2); // 120 130不输出 想拿110和130,不拿120 let [n1, ,n2] = [110,120,130] // 120没被接收 console.log(n1); // 110 console.log(n2); // 130 只想接收120 let [,n2,] = [110,120,130] console.log(n2);
02-对象的解构赋值
-
对象的解构赋值,需要保证key(键值对)一样
let {userName,userAge} = {userName:"wc",userAge:18}; console.log(userName); console.log(userAge);
-
起别名 name age
-
如果起了别名,之前的userName和userAge不能使用了
let {userName:name,userAge:age} = {userName:"wc",userAge:18}; console.log(name); console.log(age);
需要注意的问题
let {name,age} = {name:"wc"};
console.log(name); // wc
console.log(age); // undefine
// 给定默认值110,指向默认值
let {name,age=110} = {name:"wc"};
console.log(name);
console.log(age); // 110
// 如果能解构出来值,会覆盖默认值
let {name,age=110} = {name:"wc",age:666};
console.log(name);
console.log(age);
-
练习对象解构
(1)let obj = { arr:[ "hello",{ msg:"world" } ] } let { arr:[str,{msg}] } = obj; // [str,{msg}]保证和上面的arr格式一样,str可以随便写,msg不能随便写需要和上边一样 console.log(str); // hello console.log(msg); // world (2)let obj = { local:{ start:{ x:20, y:30 } } }; 解构出x、y: let{local:{start{x,y}}}=obj; console.log(x); console.log(y);
03-字符串的解构赋值
-
字符串可以当成字符数组
let str = "wangcai"; console.log(str[0]); let [a,b,c,d,e,f] = "wangcai"; console.log(a); // w console.log(b); // a console.log(c); // n let str = "wangcai"; console.log(str.length); // 7 可以打点说明wangcai瞬间包装 let { length:len } = "wangcai"; // 可以起别名 console.log(len); // 7
04-函数参数的解构赋值
-
解构一个数组
原始: function fn(arr){ console.log(arr[0]); console.log(arr[1]); console.log(arr[2]); } fn([11,22,33]) 解构: function fn([a,b,c]){ console.log(a,b,c); } fn([11,22,33])
-
解构一个对象
function fn({x,y}){ console.log(x,y); } fn({x:1,y:2}) 默认值: function fn({x=0,y=0}){ // 没有传对象就是默认值0 0 console.log(x,y); } fn({x:1,y:2})
05-解构赋值的应用
-
利用解构赋值交换两个变量
let a = 1; let b = 2; [b,a] = [a,b]; console.log("a=",a); console.log("b=",b);
-
调用一个函数,得到一个返回值,对返回值进行解构赋值
(1)返回数组 function fn(){ return ["a","b","c"] } let [n1,n2,n3] = fn(); // fn()在你眼中就是返回值(数组) console.log(n1,n2,n3); // a b c (2)返回对象 function fn(){ return { num1:111, num2:222 } } let { num1,num2 // 不能写其他的 } = fn(); console.log(num1,num2);
04-展开运算符
-
. . . 叫展开运算符,也叫扩展运算符
-
所谓的展开运算符,就是把容器中的元素一个个取出来
let arr1 = [1,2,3]; let arr2 = [4,5,6]; // let arr3 = [...arr1, ...arr2]; // 123456 let arr3 = [...arr2, ...arr1]; // 456123 console.log(arr3); 利用展开运算符求最大值 console.log(Math.max(1,2,3,1,8)); let arr = [2,1,4,7,9,2,1]; console.log(Math.max(...arr)); function fn(a,b){ return a+b; } let arr = [1,2]; console.log(fn(...arr)); // 相当于(fn(1,2))
05-rest参数
-
. . . 是rest运算符
-
args是一个数组,数组中收集到了所有的实参
function fn(a,b,c,d){ console.log(a,b,c,d); } fn(1,2,3,4); function fn(...args){ // 个数要和实参一样 // args是一个数组,数组中收集到了所有的实参 console.log(args); } fn(1,2,3,4,5,6,7); 求任意多个数组之和 遍历数组 forEach function sum(...args){ let sum = 0; args.forEach(item=>{ sum += item; }) return sum; } console.log(sum(1,2)); console.log(sum(1,2,3)); console.log(sum(1,2,3,4)); console.log(sum(1,2,3,4,5));
-
newArr
rest参数 let arr = [1,2,3,4,5,6,7,8,9]; // 解构赋值 let [n1,...newArr] = arr; // newArr除了n1,其他的都放在newArr里 console.log(n1); console.log(newArr);
-
rest参数必须位于最后一个位置
function sum(a, ...args, b){ // 报错 console.log("a:",a); console.log("args:",args); } console.log(sum(1,2,3,4,5));
06-箭头函数
- 形参只有一个可以省略(),没有或多个不可以省
- 函数体只有一行代码,{}和return可以删
- 省略{}和return,返回的对象需要用()包住
- 箭头函数中的this需要往上找一级
- 箭头函数不能当成类
- 箭头函数没有prototype属性
07-对象的扩展(copy)
01-属性和方法的简写
-
键和值一样可以省略一个
let uName = "wc"; let uAge = 18; let person = { uName:uName, uAge:uAge } console.log(person.uName); //wc console.log(person.uAge); //18 简写: let uName = "wc"; let uAge = 18; let person = { uName, uAge, } console.log(person.uName); console.log(person.uAge);
-
属性的简写
let uName = "wc"; let uAge = 18; let person = uName, uAge, eat:function(){ console.log("eat..."); } } person.eat();
-
方法的简写
let uName = "wc"; let uAge = 18; let person = { uName, uAge, eat(){ console.log("eat..."); } } person.eat();
02-对象中属性的copy
let obj1 = {
name:"wc",
address:"bj",
friend:{
name:"xq"
}
};
let obj2 = {
age:18,
socre:88
}
// 把obj1和obj2的属性copy到obj3上
let obj3 = {...obj1,...obj2};
// obj1.name = "wc666"; // 改变obj1.name值,obj3不会改变
obj1.friend.name = "xq666" // 改变obj1.friend.name,obj3会改变
console.log(obj3);
-
Object.assign 把多个对象中的属性copy到另外一个对象中
-
Object.assign浅copy
(1)copy一个对象 let obj1 = { name:"wc", address:"bj", friend:{ name:"xq" } }; let obj2 = { age:18, socre:88 } let obj3 = {}; Object.assign(obj3,obj1) obj1.friend.name = "xq666" // obj3变了 console.log(obj3); (2)copy多个对象 let obj1 = { name:"wc", address:"bj", friend:{ name:"xq" } }; let obj2 = { age:18, socre:88 } let obj3 = {}; Object.assign(obj3,obj1,obj2) // copy多个对象 obj1.friend.name = "xq666" console.log(obj3);
-
封装一个方法,实现 深copy(deepclone)
function deepclone(data){ // clone克隆 let newObj = {}; for(let key in data){ newObj[key] = data[key] } return newObj; } let obj1 = { name:"wc", address:"bj", }; let obj2 = deepclone(obj1) // 调函数 并返回给新对象obj2 console.log(obj2); 有friend(方法) function deepclone(data){ let newObj = {}; for(let key in data){ if(typeof data[key] === "object"){ // 判断是不是对象 newObj[key] = deepclone(data[key]); // 递归 }else{ newObj[key] = data[key] } } return newObj; } let obj1 = { name:"wc", address:"bj", friend:{ name:"xq" } }; let obj2 = deepclone(obj1) console.log(obj2); obj1.friend.name = "xq666" console.log(obj2.friend.name); // xq 不改变
-
如果属性名一样,后面的会覆盖前面的
let obj1 = { name:"wc", address:"bj", friend:{ name:"xq" }, a:110 }; let obj2 = { age:18, socre:88, a:220 } let obj3 = {}; Object.assign(obj3,obj1,obj2) // obj3里的是220 obj1.friend.name = "xq666" console.log(obj3); // 会改变
08-Symbol
-
一个对象中不能出现同名的属性名。如果出现,后面的会覆盖前面的
-
Object.assign 也是只能遍历可枚举的属性 —— 枚举就是遍历
-
调用Symbol函数,就可以生成唯一值
-
产生的参数"s1" "s2"叫唯一值的描述符
let s1 = Symbol("s1"); let s2 = Symbol("s2"); console.log(s1); console.log(s2); console.log(s1 == s2); // false
-
在你使用别的对象中,你有可能把别的对象中的属性覆盖了
-
如果是一个唯一值,是不可能覆盖掉
let obj = { name:"wc", age:18 } function fn(obj){ obj.id = 110 } function gn(obj){ obj.id = 220 } fn(obj) gn(obj) console.log(obj); // obj里的id是220 被覆盖 let obj = { name:"wc", age:18 } let mySymbol1 = Symbol("lib1") function fn(obj){ obj[mySymbol1] = 110 } let mySymbol2 = Symbol("lib2") function gn(obj){ obj[mySymbol2] = 220 } fn(obj) gn(obj) console.log(obj); // 不会被覆盖
09-proxy
01-Object.defineProperty拦截
-
利用Object.defineProperty也可以监听一个对象上的,属性是否被访问或设置
let obj = { name:"wc", age:18 }; Object.defineProperty(obj,"name",{ get:function(){ console.log("监听到了obj对象的name属性被访问了"); return "123456" }, set:function(){ console.log("监听到了obj对象的name属性被设置了"); } }) console.log(obj.name); // 123456 obj.name = "xq"; // 设置属性走set
-
监听多个属性
-
Object.defineProperty需要对对象中的所有的属性都进行劫持
递归遍历 let obj = { name: "wc", age: 18, address: "bj", }; // console.log(Object.keys(obj)); // Object.keys(obj)可以拿到所有键 把键放在数组里 ['name', 'age', 'address'] // ['name', 'age', 'address'] Object.keys(obj).forEach(key => { let value = obj[key]; // 拿里面的值 // console.log(value); Object.defineProperty(obj, key, { // 需要感应到,首先要重新定义name、age、和address get: function () { console.log(`监听到了obj对象的${key}属性被访问了`); return value; }, set: function () { console.log(`监听到了obj对象的${key}属性被设置了`); } }) }) console.log(obj.age); obj.age = 110; console.log(obj.address); obj.address = "sh";
02-proxy拦截get
-
proxy 代理的意思 代理器
-
target 是目标对象 property 是属性的意思
let obj = { name: "wc", age: 18 }; 根据目标对象生成一个代理对象 第一个参数是目标对象 target 第二个参数是一个处理对象 handler 放处理方法 let proxy = new Proxy(obj,{ // 在操作属性时,能不能感知点,能不能劫持到? // 当你通过代理对象去获取时,会自动走get get:function(target, property){ // console.log("get..."); // console.log(target); // console.log(property); return target[property] } }) // 通过代理对象获取name属性,能不能感知点? 能 console.log(proxy.name);
03-proxy拦截set
let obj = {
name:"wc",
age:20
}
let proxy = new Proxy(obj,{
set:function(target,prop,value){
if(prop === "age"){
if(!Number.isInteger(value)){ // isInteger判断是否是数字
// 设置的新值不是整数
// console.log("年龄必须为整数");
throw new TypeError("年龄必须为整数") // 自己创建的错误
}
if(value > 60){
throw new RangeError("年龄太大了")
}
target[prop] = value; // 正常的
}
}
})
proxy.age = 30; // 设置
console.log(proxy.age); // 获取
04-proxy总结
let obj = {
name:"wc",
age:18
};
let proxy = new Proxy(obj,{
get:function(target,key){
console.log(`监听到了obj对象的${key}属性被访问了`);
return target[key];
},
set:function(target,key,value){
console.log(`监听到了obj对象的${key}属性被设置了`);
target[key] = value;
}
})
console.log(proxy.name);
console.log(proxy.age);
proxy.name = "xq"; // 走set 设置属性
console.log(proxy.name);
05-proxy其他的捕捉器
- 对于Object.defineProperty,只能拦截get和set
- 对于Proxy,可以拦截 可以拦截13种操作 在MDN上查找
步骤如下:
-
(1)搜索MDN(developer.mozilla.org/en-US/plus)
-
(2)找JS
-
(3)内置对象——proxy——handle方法
10-set容器的使用
-
Set 和数组类似,但是元素需要唯一
-
数组中的元素可以不唯一
let arr = ["a","b","a"]
-
size也可以判断元素的个数
let s = new Set(); s.add(1) s.add(2) s.add(3) console.log(s); // {1, 2, 3} console.log(s.size);
-
has是判断一个元素是否在集合中
let s = new Set(); s.add(1) s.add(2) s.add(3) console.log(s); console.log(s.has(2)); console.log(s.has(200));
-
clear清除容器中所有的元素
let s = new Set(); s.add(1) s.add(2) s.add(3) s.clear(); // console.log(s);
-
Array.from 把一个set集合转成数组
let s = new Set(); s.add(1) s.add(2) s.add(3) console.log(s); // 把一个set结构,转成数组 console.log(Array.from(s));
-
把数组里面重复的元素去重
let arr = [1,2,1,3,2,5,3]; // new Set时,直接可以传一个数组 // 传过之后set会自动去掉 let s = new Set(arr); console.log(Array.from(s));
11-class定义类
类表达式 用的不多 了解
let Person = class {
}
let p = new Person();
console.log(p);
-
如何赋值私有属性和公有属性
class Person{ // 当new时,会自动执行constructor构造函数 constructor(name,age){ // console.log("xxxx"); // 私有属性 this.name = name; this.age = age; } } let p = new Person("xq",18); console.log(p.hasOwnProperty("name")); console.log(p.hasOwnProperty("age")); class Person{ constructor(name,age){ // 属性 this.name = name; this.age = age; } // 方法 // eating是公有属性,放在原型对象上的 eating(){ console.log("eating..."); } } let p = new Person("xq",18); console.log(p); p.eating();
-
静态属性——只能通过类名去调
在一个类中,可以写: 私有实例属性 通过对象打点去调 公有的实例属性 通过对象打点去调 静态属性 通过类名去调 静态方法 通过类名去调 class Animal{ constructor(name,age){ this.name = name; this.age = age; } // 静态属性 static score = 100; // static是静态的意思 // 静态方法 static jump(){ console.log("jump..."); } } let animal = new Animal("pig",18); Animal.jump(); // 通过类名调 console.log(Animal.score);
06-继承
01-原型继承
-
原型继承:只能继承父的公有属性
-
缺点:1)父类如果修改了的公有属性的代码,子类也会受影响
Student.prototype = new Person(); // 改变子类的原型对象 指向我们new出来的Person对象 Student.prototype.constructor = Student; // 手动修改constructor的指向 写这一行代码的目的:保证constructor指向正确
02-组合继承
-
组合继承:既继承公有属性,又继承私有属性
-
缺点:对于公有属性来说,父类变,子类也会受影响
-
Student.prototype = new Person(); Person函数执行了
-
new Student没有必须让Person执行
Person.call(this, name, age) // 此行代码继承了父的私有属性 call有两个作用:1)让Person执行 2)改变Person中的this指向
03-寄生组合继承
-
Object.create也可以创建一个对象,它可以给这个指定一个原型对象
let res2 = Object.create(null); // 没有原型对象 Student.prototype = Object.create(Person.prototype) // 此行代码继承了父的公有属性 弥补了组合继承的缺点
04-ES6中class继承
-
extends关键字就可以继承父类
子类 class Student extends Person{ constructor(name,age,className){ super(name,age); // 表示调用父类的constructor this.className = className; } }
07-Promise
01-利用回调函数解决异步问题
回调函数:代码不优雅,函数套函数。回调地狱
function execCode(couter,successCallback,failureCallback) {
// 异步代码
setTimeout(() => {
if (couter > 0) {
let total = 0;
for (let i = 0; i <= couter; i++) {
total += i;
}
successCallback(total)
} else {
failureCallback("传递数据不是正数!")
}
}, 3000)
}
execCode(100,(value)=>{ // 把100传给couter (value)=>{}传给successCallback err)=>{}传给failureCallback
console.log(value);
},(err)=>{
console.log(err);
});
02-Promise的基本语法
-
Promise作用:更加优雅解决异步问题。在ES6中是一个类,既然是类就可以new
-
创建promise方法:new一下
-
当你new的时候,需要给Promise传递一个参数,这个参数必须为函数。这个函数叫执行器 立即执行
let p = new Promise(()=>{ console.log("xxxx"); }); console.log(p);
-
一个promise有三个状态:
1)pending 等待 // 刚 new出来的promise处于等待状态 2)成功 3)失败 // 要么从等待到成功,要么从等待到失败 // 一旦成功了,就不能失败了 // 一旦失败了,就不能成功了
-
调用resolve就可以把等待的promise变成成功的promsie
-
调用reject就可以把等待的promise变成失败的promsie
let p = new Promise((resolve,reject)=>{ // 一旦成功了,不能失败 resolve("包包") reject("没钱") // 调用没用,一旦成功不能失败 }); // fulfilled 是成功的promsie // rejected 是失败的promise console.log(p);
-
在执行器中可以写异步代码
-
一个promsie对象中有一个方法,叫then
let p = new Promise((resolve,reject)=>{ // 在执行器中就可以写异步代码 setTimeout(()=>{ resolve("包包") },2000) }); // 一个promsie对象中有一个方法,叫then // ()=>{} 第一个是成功的回调 // ()=>{} 第二个是失败的回调 p.then((value)=>{ // value是成功的结果 console.log("成功的结果:",value); },(reason)=>{ // reason是失败的结果 console.log("失败的结果:",reason); });
03-resolve参数问题
-
调用resolve并不一定是把等待的promise转成 成功的promise
-
thenable就是对象中有一个then方法
-
resolve参数:
1)如果是普通数据,最终的promise是成功的promise resolve("包包") resolve(123) resolve(["a","b","c"]) 2)如果是promise,最终的promise取决于传递promise参数 resolve(p1) 3)如果是一个thenable,最终的promise取决于thenable是成功还是失败 then:function(resolve,reject){ // resolve("包包") reject("没钱") }
04-then的返回值
-
调用then方法,返回一个新的promise
-
xxx是成功还是失败,取决于你在then中做了什么???
-
then返回一个新的promise 新的promise也能then
-
新的promise then之后,又返回一个新的promise,还可以.then, 一直then下去,形成then链
-
总结:
1)如果上一个then返回一个普通数据,新的promise是成功的promsie 2)如果上一个then没有返回值,新的promise是成功的promsie 3)如果上一个then返回一个promise,新的promise是取决于返回的那个promise 4)如果上一个then返回一个thenable,新的promise是取决于返回的那个thenable 5)如果上一个then抛出一个错误,新的promise就是失败的promise
05-then的顺延
let p = new Promise((resolve,reject)=>{
// resolve("包包")
reject("没钱")
})
// p是失败了 理论上要走then的第二个回调函数
// 但是它没有第二个回调函数
// 此时,它会顺延,会走第二个then的第二个回调函数
p.then(res=>{
console.log("res1:",res);
}).then(res=>{
console.log("res2:",res);
},err=>{
console.log("err2:",err); // 顺延 失败走err2
})
-
catch是then的一个语法糖 相当于then(null,err=>{ })
-
then中一般写成功的回调,catch中写失败的回调
const promise = new Promise((resolve, reject) => { reject("bad") }); promise.then(res=>{ console.log("res:",res); }).catch(err=>{ console.log("err:",err); })
06-finally方法
无论promise是成功的,还是失败的,最终都会执行finally
const promise = new Promise((resolve, reject) => {
reject("bad")
});
promise.then(res => {
console.log(res);
}, err => {
console.log(err); // bad
throw new Error("我错了")
}).then(res => {
console.log("res:", res);
}).catch(err=>{
console.log("err:",err); // err:我错了
}).finally(()=>{
console.log("哈哈哈哈") // 哈哈哈哈
console.log("呵呵呵呵") // 呵呵呵呵
})
07-Promise的all方法
静态方法: 通过类名去调
直接创建一个成功的promise
let p = Promise.resolve("包包")
// 相当于
// new Promise((resolve) =>{
// resolve("包包")
// )} -->
p.then(res=>{
console.log("res:",res);
})
- all的作用:等到所有的promise成功后,得到成功的结果
- 如果有一个失败了,得到最先失败的promise结果
08-Promise的allSettled方法
- allSettled 获取所有Promise的结果,不管成功还是失败
09-Promise的race方法
- 得到最快的那个promise的结果,不管它是成功的,还是失败的
10-Promise的any方法
- 获取最先成功的promise
- 如果所有的promise都失败了,得到 AggregateError: All promises were rejected
11-async函数
-
async是一个关键字,用于声明一个异步函数,async是asynchronous简写,是异步的意思。
-
sync是synchronous简写,是同步的意思。
-
async函数一定是返回一个promise
fulfilled 是成功的意思 普通函数可以使用async async function fn(){ console.log("fn..."); console.log("fn..."); console.log("fn..."); } // 调用时,和之前是一样的 let res = fn(); console.log(res);
-
函数表达式也可以使用async
let gn = async function(){ console.log("gn..."); } let res = gn(); console.log(res);
-
箭头函数也可以使用async
let gn = async ()=>{ console.log("gn..."); } let res = gn(); console.log(res);
-
方法也可以是async函数
class Person{ // 方法也可以是async函数 async run(){ console.log("run..."); } } let p1 = new Person(); console.log(p1.run());
12-async函数的返回值
async function foo(){
console.log("foo...");
console.log("foo...");
console.log("foo...");
// 1)返回普通值 promise就是成功的promise
// return 123;
// return undefined;
// return [1,2,3]
// 2)返回Promise res这个promise取决于你返回的promise
// return new Promise((resolve,reject)=>{
// // resolve("包包")
// reject("没钱")
// })
// 3)返回thenable
// return {
// then:function(resolve,reject){
// reject("没钱")
// }
// }
// 4)抛出一个错误
throw new Error("我是一个错误~") // 失败的Promise
}
let res = foo();
// console.log("res:",res);
res.then(res=>{
console.log("res:",res);
}).catch(err=>{
console.log("err:",err);
})
13-await关键字
在异步函数内部可以使用await关键字,但是在普通函数中不能使用await关键字
- await只能出现在async函数中
await后面跟不同的数据:
-
如果await后面跟一个普通值,那么会直接返回这个值。
-
如果await后面跟一个thenable对象,那么要看你这个thenable中的then做了什么。
-
如果await后面的promise是失败的,需要通过try catch来获取失败的结果
let p = new Promise((resolve, reject) => { setTimeout(() => { // resolve("包包") reject("没钱") }, 3000) }) async function fn() { try { let res = await p; console.log("res:", res); }catch(err){ console.log("err:",err); } } fn();
14-什么是事件环
- 同步代码、异步代码(耗时任务)
异步代码分两类:
- 宏任务:ajax,setTimeout,setInterval,DOM事件监听,UI渲染,setImmediate....
- 微任务:promies中的then回调,Mutaion Observer,queueMicrotask,process.nextTick,await下面的都是微任务...
- setImmediate和setTimeout执行顺序不确定
JS代码的执行顺序:——构成循环(事件环)
- 从代码段开始执行
- 如果遇到一个宏任务,会把这个任务放到一个宏任务队列,如果遇到一个微任务,就把这个微任务放到微任务队列中。
- 当同步代码执行完毕后,先去清空微任务队列。
- 当微任务队列清空完毕后,从宏任务队列中取出一个宏任务,去执行,在执行过程中,你的宏任务中可能还有同步代码或宏任务或微任务,重复上面的步骤,执行完一个宏任务,肯定要清空微任务队列。
练习题:
console.log(1)
setTimeout(() => {
console.log(2)
Promise.resolve().then(() => {
console.log(3) // 清空任务 打印出3
})
})
setTimeout(() => {
console.log(4)
Promise.resolve().then(() => {
console.log(5) // 清空任务 打印出5
})
})
Promise.resolve().then(() => {
console.log(6)
08-手写系列
面试题
01-new的原理
new 做了什么? 出是一个面试题
1)在构造器内部创建一个新的对象
2)这个对象内部的__proto__属性会被赋值为该构造函数的prototype属性;
3)让构造器中的this指向这个对象
4)执行构造器中的代码
5)如果构造器没有返回对象,则返回上面创建出来的对象
new 是一个运算符 如何去模拟一个运算符
只能通过函数去模拟这个运算符
02-call的原理
call做什么?1)改变this指向 2)让函数执行
03-apply的原理
apply的作用和call是一样的 只不过传递的参数是以数组的形式
04-bind的原理
bind做了什么?
1)改变this指向
2)没有让函数执行,返回一个改变this指向后的函数。 返回函数就需要定义一个新函数gn
05-map原理
-
map自带循环功能 map对数组中的元素进行加工 得到一个加工后的新数组
-
每加共一个元素放在新的数组里,一次循环,最后打印出新数组
var arr = [1, 3, 6, 90, 23]; // ele表示数组中的每一个元素 // index表示数组中元素的索引 // array表示数组 var result = arr.map(function(ele,index,array){ // console.log("ele:",ele); // console.log("index:",index); // console.log("array:",array); return ele * ele; // 加工后的新数组放在result里 }) console.log("result: ===", result); // 打印result [1, 9, 36, 8100, 529]
06-find原理
find找到满足条件的第1个元素
find的返回值,是数组中的某个元素
如果找不到满足条件的元素,返回und
07-filter原理
返回一个新数组
新数组中满足条件的元素
08-some原理
只要有一个元素满足条件,结果就是true
09-every原理
every中,如果所有元素都满足条件,结果就是true
只要有一个不满足,结果就是false
10-reduce原理
reduce是累加器
var arr = [1, 2, 3, 4, 5, 6];
// 第一次:prev:1 next:2
// 第二次: prev:3 next:3 prev + next
// 第三次: prev:6 next:4
// 第四次: prev:10 next:5
// 第五次: prev:15 next:6
// 第六次: prev:21 next:无 数组里next只到6
var sum = arr.reduce(function (prev, next) {
return prev + next;
});
console.log(sum);
指定初始的prev为100
var arr = [1, 2, 3, 4, 5, 6];
// 第一次:prev:100 next:1
// 第二次: prev:101 next:2
// 第三次: prev:103 next:3
// 第四次: prev:106 next:4
// 第五次: prev:110 next:5
// 第六次: prev:115 next:6
// 第七次: prev:121 next:无
var sum = arr.reduce(function (prev, next) {
return prev + next;
}, 100);
console.log(sum);
11-findIndex原理
findIndex找到满足条件的第一个 返回这个元素的索引1
如果找不到,返回-1
12-concat原理
合并多个数组成一个数组,参数也可以是字符串
13-什么是防抖——debounce
手写防抖
14-什么是节流——throttle
不管按多快,都是每隔1秒发一次。高频无法影响,和频率无关
手写节流
09-其他
01-JSON
认识JSON
- JSON: JavaScript Object Notation JS对象描述符
- JSON是服务器响应给客户端的数据格式 也可以把JSON数据给服务器
- 所以说 JSON是客户端与服务器之间通信的一种数据格式。
- 是目前最最常用的数据格式。
JSON的格式: 创建文件,后缀为json
格式一
"hello wc"
格式二
对象的key必须使用双引号包起来,且最后一个属性没有逗号
{
"name": "wc",
"age:": 19,
"friend": {
"name": "xq"
},
"hobbies":["zq","lq"]
}
格式三
把对象放在数组里
[
"abc",123,{"name:":"wc"}
]
JSON的语法要求:
- 支持的数据类型:数字,字符串,布尔值,null值,不能写函数
- 对象的key必须使用双引号包起来
- JSON中没有注释
- 通过一些网站可以验证自己的JSON是否OK(索www.bejson.com/ 把内容粘贴进去)
01-JSON的序列化
- 利用localStorage.setItem,就可以把数据存储在硬盘上.无论是断电还是关闭程序,数据一直都在
- 把一个对象数据存储到localStorage时,必须是以字符串的形式存储
- 如果不是字符串,那么它会转化成字符串
- JSON.stringify 可以把一个对象转成一个JSON字符串
把obj对象转成JSON串之后,再存储,这个过程,叫序列化。
let obj = {name:"wc"};
// JSON.stringify 可以把一个对象转成一个JSON字符串
let objstr = JSON.stringify(obj)
console.log(objstr);
console.log(typeof objstr);
// 利用setItem,把ISON串存进去
window.localStorage.setItem("objstr",objstr)
02--JSON的反序列化
- JSON.parse,把JSON串转成对象
- 利用getItem,把ISON串取出来
反序列化:把JSON串还原成JS对象
let jsonStr = localStorage.getItem("jsonStr");
// 取出来的,也是字符串
console.log(jsonStr);
// 把JSON串,转成对象JSON.parse
let jsonObj = JSON.parse(jsonStr);
console.log(jsonObj);
-
利用JSON可以实现深copy
let myObj = { name:"wc", friend:{ name:"xq" } }; // JSON.parse(JSON.stringify(myObj)) 实现深copy let newObj = JSON.parse(JSON.stringify(myObj)) myObj.friend.name = "xq666"; console.log(newObj); // 是name:"xq",而不是name:"xq666" 实现深coppy
02-数据存储
localStorage.setItem("name","z3") // 存数据
localStorage.setItem("name","l4")
console.log(localStorage.getItem("name")); // 拿数据
localStorage.removeItem("name") // 删除数据
localStorage.setItem("name","z3")
localStorage.setItem("age","18")
localStorage.setItem("sex","man")
console.log(localStorage.length); // 返回数据的数量
// localStorage.clear(); // 清除localStorage中的所有数据
-
持久化存储数据
-
Local Storage,Session Storage,Cookies
-
sessionStorage与localStrorage区别:
sessionStorage浏览器关了数据就没了
使用自己封装的工具类
封装类
class Cache{
// 对于形参可以指定默认值
constructor(isLocal = true){
this.storage = isLocal ? localStorage : sessionCache;
// isLocal是true,用localStorage
}
// 存
setItem(key,value){
if(value){
this.storage.setItem(key,JSON.stringify(value))
}
}
// 取
getItem(key){
let value = this.storage.getItem(key);
// 判断有没有value代表的值,有的话进入if
if(value){
value = JSON.parse(value)
return value;
}
}
// 删除
removeItem(key){
this.storage.removeItem(key)
}
// 清空所有
clear(){
this.storage.clear();
}
// 数量
length(){
return this.storage.length;
}
}
// localCache 对 localStorage的操作
// let localCache = new Cache(true);
// let localCache = new Cache(); // 什么都没传,默认是true
// sessionCache 对 sessionStorage的操作
// let sessionCache = new Cache(false);
使用
<script src="../utils/cache.js"></script>
<script>
let obj = {
name:"wc"
};
// 把序列化和反序列化封装在cache里
let localCache = new Cache();
localCache.setItem("ok",obj) // "ok"传给key,obj传给value 不用序列化,封装的时候已经序列了
console.log(localCache.getItem("ok")); // 取 不用反序列化,封装的时候已经...
localCache.removeItem("ok") // 删除
localCache.clear(); // 清空所有
</script>
03-JS中常见的错误
-
语法错误
最好解决 你写的JS代码,不符合人家的要求 SyntaxError: Function statements require a function name function fn(){}
-
引用错误
ReferenceError: b is not defined console.log(b);
-
范围错误
RangeError: Invalid array length let arr = new Array(-10); console.log(arr);
自己抛出一个错误并捕捉错误
自己抛出一个错误 throw后面代码不再执行
function sum(num1,num2){
// 传的数据都是number
if(typeof num1 !== "number" || typeof num2 !== "number"){
// return "你的参数不合适,请传递number类型的数据~";
// throw也是扔到函数的调用处,后面代码不再执行
throw "你的参数不合适,请传递number类型的数据~";
}
return num1 + num2;
}
console.log(sum(1,2));
console.log(sum("1",undefined));
console.log(sum(null,null)); // 0
console.log("------后面的代码------");
try catch后面代码会执行
new 一个Error,或TypeError
function sum(num1, num2) {
if (typeof num1 !== "number" || typeof num2 !== "number") {
throw new TypeError("你的参数不合适,请传递number类型的数据~");
}
return num1 + num2;
}
// 下面的代码,是有可能出错的
// 所谓的有可能是指,可能出错,也可能不出错 try catch
// 后面代码会执行
try {
// try中写可能出错的代码
console.log(sum(1, 2));
console.log(sum("1", undefined));
console.log(sum(null, null));
}catch(err){
// catch中就可以捕获到别人抛出的错误
console.log(err);
}
console.log("------后面的代码------");
04-with,eval
01-with
作用域链:
-
作用域链就是数据在EC中的查找机制
原型链:
-
对象中属性的查找机制
// a.b // 找a 作用域链 // 找b 原型链
-
with语句,可以提供一个作用域。obj就是它提供的作用域
let msg = "xixi"; let obj = {naem:"wc",msg:"haha"} function foo(){ let msg = "hehe" function bar(){ let msg = "heihei" with(obj){ console.log(msg); // haha 按照以前子类没有去父类里找,但with需要去obj里找 } } bar() } foo()
02-eval
-
eval是一个特殊的函数,作用:可以把一片字符串,当成JS代码运行
let age = 110; // 能不能把一个字符串当成JS代码去运行呢? // 答:可以 使用eval let jsStr = " var msg = 'haha'; console.log(msg); console.log(age); "; // haha 110 eval(jsStr); console.log(msg); // 不加eval不可以
05-严格模式
在哪开启:
-
在一个JS文件中开启 “use strict” 这个文件中写的代码都受严格模式的约束
-
在一个函数中开启格式模式 function fn(){ “use strict” xxx } 其它代码不受约束
<script> 1)不能使用没有加var的变量 "use strict" // 开启严格模式,不能使用没有加var的变量 // ReferenceError: uname is not defined uname = "wc"; console.log(uname); </script> <script> 2)形参不能重名 "use strict" // 开启严格模式,形参不能重名 // SyntaxError: Duplicate parameter name not allowed in this context function fn(a,b,a){ console.log(a); console.log(b); console.log(a); } fn(1,2,3) </script> 3)不能使用老的8进制数据的写法 let num1 = 0x123; // 16进制 let num2 = 0o10; // 8进制(新的8进制写法) o开头 let num3 = 010; // 8进制(老的8进制写法) console.log(num1); console.log(num2); console.log(num3); <script> "use strict" // 不能使用with语句 let obj = { msg: "hello" } with (obj) { console.log(msg); } </script> <script> let age = 110; let jsStr = " 'use strict'; var msg = 'haha'; console.log(age); "; // 开严格模式 eval(jsStr); console.log(msg); // 不能使用 </script> <script> "use strict" function fn() { console.log(this); // 独立函数调用,this表示und } fn(); </script> <script> function fn() { // 在函数内部去开启严格模式 "use strict"; a = 110; console.log(a); // 没有var报错 } fn(); </script> <script> function gn(){ a = 123; console.log(a); // 约束不了gn,能打印出来123 } function fn() { // fn内部是严格模式 但是gn内部不是严格模式 "use strict"; gn(); } fn(); </script>
06-函数的柯里化
// 未柯里化的函数
function add(x,y,z){
return x+y+z;
}
console.log(add(20,10,30));
// 柯里化处理的函数
function add2(x){
return function(y){
return function(z){
return x+y+z;
}
}
}
console.log(add2(10)(20)(30));
// 简写 箭头函数
let add = x=>y=>z=>x+y+z;
console.log(add2(10)(20)(30));
手写实现函数的柯里化
// curring 柯里化的意思
function curring(fn,...args1){
// fn.length 表示形参个数
// console.log(fn.length); 3个参数
let length = fn.length;
let allArgs = [...args1]; // 收集参数
let gn = (...args2)=>{
allArgs = [...allArgs, ...args2]
console.log("allArgs:",allArgs);
// console.log("allArgs:",allArgs);
if(allArgs.length == length){
// 参数收集完毕
return fn(...allArgs);
}else{
// 参数没有收集完 返回新函数
return gn;
}
}
return gn; // newAdd
}
// 没有经过柯里化处理
let add = (a,b,c) => a+b+c;
// newAdd是柯里化后的函数
let newAdd = curring(add,1); // add传给fn
// console.log(newAdd(2,3)); // 2传给b,3传给c
console.log(newAdd(2)(3));
07-组合函数
function double(num){
return num*2;
}
function square(num){
return num ** 2;
}
let count = 12;
let res = square(double(count))
console.log(res);
手写compose函数
// 实现一个compose函数
// compose是组合的意思
function fn1(x) {
return x + 1;
}
function fn2(x) {
return x + 2;
}
function fn3(x) {
return x + 3;
}
function fn4(x) {
return x + 4;
}
function compose(...fns){ // fns多个函数
if(fns.length === 0){ // 没有传函数
return (num)=>num;
}
if(fns.length === 1){ // 传一个函数
return fns[0]
}
// reduce是累加器
return fns.reduce((prev,next)=>{
// fns [fn1,fn2,fn3,fn4,fn5,fn6.....]
return (num)=>{
return next(prev(num)) // 第一次prev是fn1
}
})
}
// let res = compose();
// let res = compose(fn1);
let res = compose(fn1,fn2,fn3,fn4); // return next(prev(num))给res
// res就是组合后的函数 fn4(fn3(fn2(fn1(1))))
console.log(res(1));
console.log("------------");
console.log(fn4(fn3(fn2(fn1(1))))); // 11