JAVASCRIPT部分
📙说说JavaScript数据类型
JavaScript中的数据类型主要有8中,分别是7种基本数据类型以及1种引用数据类型:Null
Undefined
Boolean
Number
BigInt
String
Symbol
和 Object
。JavaScript的类型系统松散,没有严格类型要求,因此我们需要借助typeOf来判断数据类型。typeOf除了可以判断这8种数据类型,也可以判断函数类型,虽然函数本质是一个对象,但是它具有自己特有 的属性。但是typeOf运算符也存在一定的问题,比如:typeof null
返回object。
Null
定义:空对象指针,常用来给将要以对象类型保存的变量进行初始化
要点:因此,这也是typeOf null
返回object的原因
Undefined
定义:为了区分空指针对象 和 未初始化变量,由null
派生除了undefined
。因此,在ECMA-262种,console.log(undefined==null)
返回true。
要点:但是undefined的问题在于,它无法区分 未初始化变量 以及 未声明变量 两者都返回undefined。因此,建议在声明时进行初始化。
Boolean
定义:布尔值返回两个字面量 true
和 false
,这两个字面量区分大小写
要点:虽然布尔值只有两个,但是其他数据类型都有布尔值的等价形式。
数据类型 | 转换为true值 | 转换为false值 |
---|---|---|
undefined | 不存在 | undefined |
number | 除0以外数值 | 0 |
string | 非空字符串 | 空字符串 |
object | 任意对象 | null |
Number
进制:Number
默认10进制,除10进制以外,以0开头为8进制,以0x开头为16进制
浮点数:
-
浮点数的小数点后至少有一位数字
-
但是,由于浮点数的储存空间是整数的两倍,因此,类似于
1.
小数点后没有数字 或10.00
这种可以保存为整数的数字会以整数保存 -
并且,注意不要使用
if(a+b==0.3)
这种浮点数的判断,因为浮点数的计算精度不如整数高,当a取0.1,b取0.2时得到的结果并不等等于0.3
两种特殊的number:
Infinity
在js种数值存在一定范围,当数字超过Number.MAX_VALUE
或 小于Number.MIN_VALUE
时就会返回正负Infinity
,需要注意的是:
--- `Infinity`不能再进行任何的计算
---可以使用`isInfinity()` 函数判断数值是否有限
NaN
翻译为(not a number),表示不是数值,例如:0除以0则会返回NaN。需要注意的是:
---任何涉及NaN的计算操作始终返回NaN
---NaN不等于包括自身在内的任何值
---可以使用isNaN()来判断是否不是数值,但是isNaN()函数会尝试把参数转换为数值类型。比如:在`isNaN('10')`中,返回值为false,会将字符串10自动转化为数字。
数值转换
转换为数值类型主要有3种方法:Number()
、parseInt()
、parseFloat()
,Number()
可以用于任意数据类型,而后两者主要用于字符串转换。
使用Number()
进行数值转换时,null返回0、undefined返回NaN、字符串返回所包含的数值,空字符串则返回0、对象先调用valueOf()
方法进行字符串转换,如果转换结果为NaN,则调用toString()
方法。`
parseInt()
可以转换为整数数值,不同点在于:①使用parseInt()
进行类型转换时,只要第一个字符不是数值字符或加减号,则立即返回NaN。②并且,parseInt()
可以传入第二个可选参数用于指定进制数。
parseFloat()
和parseInt()
工作方式类似,①但是,前者始终忽略字符串开头的0 ②并且不能传入第二参数指定进制数,只能转换10进制值。③如果转换结果为整数,则以整数保存
BigInt
由于Number具有一定的范围,但是有些时候我们需要使用超范围的值进行计算。因此,我们引入BigInt
数据类型
要点:
-
BigInt不能与Number类型进行混合运算
-
BigInt转换为Number有可能会丢失精度
-
BigInt可以和Number进行比较,但是相同值的BigInt和Number只宽松相等并不严格相等
-
对BigInt的所有操作返回也是BigInt
String
string主要有三种形式:单引号、双引号、反引号(``)
分配内存:字符串一旦创建就无法改变,如果要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量
特殊字符:例如-换行符写作‘\n’、制表符写作’\t’、\b
, \f
, \v
退格,换页,垂直标签 —— 为了兼容性,现在已经不使用了
模板字面量:ES6中新增了使用模板字面量(反引号)定义字符串。
①使用模板字面量最大的特点就是可以跨行定义字符串。但是它保留反引号内部空格,因此使用的时候需要注意
②在反引号内部,可以通过${}符号插入一些表达式或值,所有插入的值都会通过toStirng()转换为字符串
类型转换:转换成字符串主要有两种方法String()
和 toString()
toString():①null和undefined没有toString方法 ②通常情况下,不接受参数,但是进行数值类型的转换时可以接收一个参数,指定进制数
String():①如果有toString()
则调用该方法 ②null转换为“null”,undefined转换为“undefined”
Symbol
创建对象的唯一标识符。
-
Symbol() 函数不能与 new 关键字一起作为构造函数使用
-
每一次通过Symbol()函数创建的Symbol都是唯一的
-
使用
Symbol.for()
方法和Symbol.keyFor()
方法从全局的 symbol 注册表设置和取得 symbol -
常用Symbol()函数创建实例用作对象属性,确保不会覆盖原有属性
Object
对象就是一组数据和功能的合集。Object(Array、Object、Function、Map、Set)
-
constructor :用于创建当前对象的函数
-
hasOwnProperty(propertyName) :用于判断当前对象实例(不是原型)上是否存在给定的属性。
-
isPrototypeOf(object) :用于判断当前对象是否为另一个对象的原型
-
propertyIsEnumerable(propertyName) :用于判断给定的属性是否可以使用(属性名必须是字符串)
-
toLocaleString() :返回对象的字符串表示
-
toString() :返回对象的字符串表示。
-
valueOf() :返回对象对应的字符串、数值或布尔值表示
类型存储
-
引用类型存储在堆中,基本数据类型存储在栈中
-
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址。比如:
var obj1 = {}
var obj2 = obj1;//赋值为引用地址
obj2.name = "Xxx";
console.log(obj1.name); // xxx
📕什么是作用域,作用域与作用域链
作用域
作用域可以理解为执行环境中变量或函数的作用范围,决定了特定部分中变量、函数和对象的可访问性。作用域主要有三种:全局作用域、函数作用域 和 块级作用域
全局作用域
生命周期:全局作用域在页面打开时被创建,页面关闭时被销毁
要点:
- 编写在script标签中的变量和函数,作用域为全局
- 全局作用域可以认为是window,因为所有全局变量和函数都是作为window对象的属性和方法创建的
- 所有末定义直接赋值的变量自动声明为拥有全局作用域
问题:容易污染全局命名空间, 引起命名冲突
函数作用域
生命周期:函数调用时,函数作用域被创建,函数执行完毕,函数作用域被销毁
要点:
- 每调用一次函数就会创建一个新的函数作用域,他们之间是相互独立的
- 函数作用域可以访问上层作用域,但相邻函数作用域是相互独立的
- ES6之前没有块级作用域,因此,块语句(大括号“{}”中间的语句),如判断循环语句,它们不会创建一个新的作用域
if (true) {
// 'if' 条件语句块不会创建一个新的作用域
var name = 'Hammad'; // name 依然在全局作用域中
}
console.log(name); // logs 'Hammad'
块级作用域
这种变量提升行为在开发中容易产生bug,因此ES6引入了let
和const
关键字,在大括号中使用let
和const
声明的变量存在于块级作用域中(变量外大括号)。与其他作用域一样,变量对外是不可见的。
if (true) {
let name = 'Hammad'; // name 在块级作用域中
}
console.log(name); // undefined
作用域链
当我们需要使用一个当前作用域没有定义的变量时(自由变量),就需要向父级作用域寻找,如果父级作用域也没有就层层向上查找,这种层层查找的关联关系就是作用域链。
在这里其实父级作用域的说法是有一定的不全面,由于JavaScript支持的是词法作用域(静态作用域)作用域在词法化阶段确定,而不是在执行阶段确定。因此前面说的父级作用域,是创建函数时的作用域的父级作用域,而不是执行时的父级作用域。
工作原理:首先在创建fn函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。当调用fn函数时,会为函数创建一个执行上下文,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链,然后创建活动对象(每个执行上下文都会有一个包含其中变量的对象,全局上下文中叫变量对象,函数局部上下文中叫活动对象),并推入执行环境的作用域链。在fn执行完成后,函数活动对象会被摧毁,只剩下全局作用域。
📕什么是闭包
定义:一个函数有权访问另一个函数的变量对象就称之为闭包,其本质是作用域链的一种体现。
分析常见闭包执行流程:
function books () {
var book = '书包里面的书本'
return function () {
console.log(book)
}
}
var bag = books()
bag()
-
全局执行上下文创建作用域链,作用域链包含了全局变量对象 [作用域链:[全局变量对象]]
-
books函数调用时创建作用域链,具体操作为先复制全局的作用域,然后创建活动对象AO推入当前作用域的顶端 [作用域链:[book活动变量, 全局变量对象]],books函数执行完毕后当前作用域会被销毁
-
bag函数调用创建作用域,首先复制上层作用域[作用域链:[book活动变量, 全局变量对象]],然后创建活动对象AO推入当前作用域的顶端[作用域链:[匿名函数func, book活动变量, 全局变量对象]],此时,由于bag对books还有引用,因此books活动变量还没有销毁,当bag函数执行完毕后当前作用域会被销毁
常见闭包
-
将一个函数作为另一个函数返回值
function foo() { var name = 'foo' return function bar() { console.log(name) } } var fn = foo() fn()
作用:foo外部访问函数内部变量
-
使用函数赋值:当一个函数赋值需要使用另一个函数中的变量,在闭包里面给
fn2
函数设置值var fn2; function fn(){ var name="hello"; //将函数赋值给fn2 fn2 = function(){ return name; } } fn()//要先执行进行赋值, console.log(fn2())//执行输出fn2 hello
-
柯里化函数:避免频繁调用具有相同参数函数的同时,又能够轻松的重用
// 假设我们有一个求长方形面积的函数 function getArea(width, height) { return width * height } // 如果我们碰到的长方形的宽老是10 const area1 = getArea(10, 20) const area2 = getArea(10, 30) const area3 = getArea(10, 40) // 我们可以使用闭包柯里化这个计算面积的函数 function getArea(width) { return height => { return width * height } } const getTenWidthArea = getArea(10) // 之后碰到宽度为10的长方形就可以这样计算面积 const area1 = getTenWidthArea(20) // 而且如果遇到宽度偶尔变化也可以轻松复用 const getTwentyWidthArea = getArea(20)
-
模拟私有变量:由于JavaScript无法实现私有变量的定义和使用,因此直接在类里面定义变量是十分不安全的,可能当我们获取对象时,直接能获取有些需要隐藏的属性(如:密码等)。因此,我们需要通过将类的外层包裹立即执行函数实现闭包,从而模拟类的私有属性
// 利用IIFE生成闭包,返回user类 const User = (function () { // 定义私有变量_password let _password class User { constructor(username, password) { // 初始化私有变量_password _password = password this.username = username } login() { console.log(this.username, _password) } } return User })() let user = new User('小明',123465) console.log(user.username); // 小明 console.log(user.password); // undefined console.log(user._password); //undefined user.login(); // 小明 undefined
其他还有例如:节流和防抖、回调函数、延迟调用等。
闭包存在的问题:
内存占用:由于形成闭包时,函数作用域链上会包括所有包含该函数的变量对象,因此,比较占用内存
内存泄漏:内存泄漏是指不再用到的内存,没有及时释放。比如:当我们在闭包中涉及循环引用等内容,当这个引用函数没有销毁前,只要触发就涉及到对外层函数变量的引用,因此该变量一直不能销毁。可以通过将存在引用关系的变量值设为null回收该变量。
📕预编译的过程有哪些
预编译是上下文创建之后, js代码执行前的一段时期, 在这个时期, 会对js代码进行预处理
全局预编译
全局上下文创建后,会生成变量对象VO
-
VO首先寻找变量声明,将var声明的变量作为VO对象的属性名,值为undefined
-
然后寻找函数声明,属性值为函数本身
-
如果函数名与变量名冲突,函数声明会将变量声明覆盖。
console.log(a)//function var a = 100; function a () { } console.log(a)//100
变量声明—>函数声明
函数预编译
函数上下文创建后,会生成变量对象AO(活动对象)
-
寻找变量声明, 变量名作为AO对象的属性名, 属性值置为 undefined
-
寻找形参, 形参名作为AO对象的属性名, 属性值置为 undefined
-
将实参的值赋予形参, 即替换 AO对象中形参的属性值
-
寻找函数声明, 函数名作为AO对象的属性名, 属性值为函数本身
-
如果函数名与变量名冲突, 函数声明会将变量声明覆盖
function a(b, c) { console.log(b); //1 var b = 0 console.log(b); //0 var b = function () { console.log('bbbb') } console.log(c); //undefined console.log(b); //function } a(1)
变量声明—>形参—>实参—>函数声明
几道面试题(实际演练🏆)
function fn (a, c) {
console.log(a)//function
var a = 123
console.log(a)//123
console.log(c)//function
function a() {}
if (false) {
var d = 678
}
console.log(d)//undefined
console.log(b)//undefined
var b = function () {}
console.log(b)//function
function c () {}
console.log(c)//function
}
fn(1, 2)
- 所有var声明的变量会声明提前,赋值为undefined,覆盖了全局的foo
var foo = 1;
function bar() {
//由于var变量声明提前,这相当于有一个var foo,但是没有赋值
console.log(foo); //undefined
if (!foo) {
var foo = 10; //相当于 foo = 10;
}
console.log(foo); //10
}
bar();
-
使用函数声明定义的函数会声明提前,可以随意在函数前后调用
-
函数表达式不能执行的原因是:var声明提前,但是赋值undefined,undefined()会报错
function fn () {
func() //声明式
//函数表达式 这里会覆盖提前的函数声明
var func = function () {
console.log('表达式')
}
//函数声明
function func() {
console.log('声明式')
}
func() //表达式 原因:函数表达式将函数声明覆盖了
}
fn()
function test(d) {
console.log(b);
if (a) {
b = 100;
}
console.log(b);
c = 4;
console.log(d);//undefined
var d = 20;
console.log(d);//20
}
var a = 10;
var b = 10;
test(3);
console.log(c);
📕深拷贝与浅拷贝
JavaScript中存在两大数据类型:基本数据类型 和 引用数据类型。基本数据类型存储在栈空间中,引用数据类型存储在堆内存中,其变量是指向堆内存中实际对象的引用地址值。
赋值
当我们把一个对象赋值给一个新的变量时,复制的是对象在栈内存中的引用地址值,而不是堆中的数据。因此无论哪个对象发生改变,都会改变堆中实际对象的内容。
浅拷贝
对一个对象进行浅拷贝时,如果数据是基本数据类型,则会直接拷贝数据值。而如果数据是引用数据类型,则会拷贝引用地址值。
在JavaScript中存在浅拷贝的现象有:
Object.assign()
Array.prototype.concat()
Array.prototype.slice()
- 拓展运算符
深拷贝
深拷贝会在堆空间中创建一个新的对象,两个对象属性完全相同,但是两个栈中变量对应着两个不同的地址。
常用深拷贝的方式有:
-
lodash中定义的_.cloneDeep()方法
const _ = require('lodash'); const obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3] }; const obj2 = _.cloneDeep(obj1); console.log(obj1.b.f === obj2.b.f);// false
-
jQuery.extend()
const $ = require('jquery'); const obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3 ]}; //extend 第一个参数:是否深度拷贝,默认false 第二个:合并的目标对象 //第三个以后:可设置多个被合并对象 const obj2 = $.extend(true, {}, obj1); console.log(obj1.b.f === obj2.b.f); // false
-
JSON.stringify()
const obj2=JSON.parse(JSON.stringify(obj1));
弊端:会忽略undefined
、symbol
和函数
- 手写循环递归
function deepClone(obj = {}) {
if (typeof obj !== 'object' || obj == null) {
// obj 是 null ,或者不是对象和数组,直接返回 函数不用深拷贝
return obj
}
// 初始化返回结果
let result
if (obj instanceof Array) {
result = []
} else {
result = {}
}
for (let key in obj) {
// 保证 key 不是原型的属性
if (obj.hasOwnProperty(key)) {
// 递归调用!!!
result[key] = deepClone(obj[key])
}
}
// 返回结果
return result
}
📕谈谈对this对象的理解
函数是一个单独的值,可以在不同的上下文环境中执行。JavaScript 允许在函数体内部,引用当前环境的其他变量。因此需要this在函数体内部能够获得函数当前的运行环境。
this绑定规则
全局环境下:
-
浏览器的全局环境下,无论是否严格模式,都指向
window
-
普通函数,非严格模式下,指向
window
。严格模式下,指向undefined
。
function f() {
console.log(this === window);
}
f() // true
function f() {
'use strict';
console.log(this === undefined);
}
f() // true
构造函数中:
-
构造函数中this指的是创建的实例对象
-
如果构造函数返回的是一个对象,
new
以后this指向返回的对象function fn() { this.user = 'xxx'; return {}; } var a = new fn(); console.log(a.user); //undefined
-
如果构造函数返回的是一个简单对象或
null
时,此时new以后this仍然指向创建实例对象function fn() { this.user = 'xxx'; return 1; //或者return null } var a = new fn; console.log(a.user); //xxx
对象方法中:
-
当函数作为某个对象的方法时调用,this指向这个上级对象。
-
当对象调用内层对象下的函数,尽管这个函数是被最外层的对象所调用,
this
指向的也只是它上一级的对象var o = { a:10, b:{ fn:function(){ console.log(this.a); //undefined } } } o.b.fn(); //上述代码中,this的上一级对象为b,b内部并没有a变量的定义,所以输出undefined
-
this
永远指向的是最后调用它的对象var o = { a:10, b:{ a:12, fn:function(){ console.log(this.a); //undefined console.log(this); //window } } } var j = o.b.fn; j(); //虽然fn是对象b的方法,但是fn赋值给j时候并没有执行,所以最终指向window
显示修改this指向:
可以使用apply()、call()、bind()
调用并修改函数指向,第一个参数就表示改变后的调用这个函数的对象。因此,this
指向第一个参数对象。
箭头函数中的this
-
箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this
-
箭头函数体内的 this 对象(固定化),就是定义时所在的对象,而不是使用时所在的对象。
-
call() / apply() / bind()
方法对于箭头函数来说只是传入参数,对它的 this 毫无影响
数组中的this
数组的map
和foreach
方法,允许提供一个函数作为参数。这个函数内部this
指向window
对象。但是map
和foreach
方法可以传入第二个参数确定this指向,也可以使用箭头函数解决
原型链上的this
当使用实例调用实例上没有定义的函数时,就会向上查找原型链上是否有该函数,如果从原型链上调用该函数,函数内的this指向仍指向调用函数的对象
var o = {
f : function(){
return this.a + this.b;
}
};
var p = Object.create(o);
p.a = 1;
p.b = 4;
console.log(p.f()); // 5
setTimeout & setInterval中的this
对于延时函数内部的回调函数的this指向全局对象window(当然我们可以通过bind方法改变其内部函数的this指向)
一道练习题
function Foo(){
//这个创建的是全局的getName()
getName = function(){ console.log(1); };
return this;
}
Foo.getName = function(){ console.log(2); };
Foo.prototype.getName = function(){console.log(3); };
var getName = function(){ console.log(4); };
function getName(){ console.log(5) };
//因为
Foo.getName(); //2
getName(); //4
//调用Foo()返回window的this,然后调用构造函数中全局的getName
Foo().getName();//1
getName();//1 因为全局被上面那个覆盖
//这里是 new (Foo.getName())
new Foo.getName();//2 调用的是构造函数中的getName
new Foo().getName();//3 Foo没有getName属性,所以去原型上找 先new Foo() 再.getName()
new new Foo().getName();//3 相当于 new ((new Foo()).getName())()
📕谈谈bind、call、apply 区别?如何实现一个bind?
call
、apply
、bind
作用都是改变函数执行时的上下文。
apply
参数:apply
接受两个参数,第一个参数是this
的指向,第二个参数是函数接受的参数,以数组的形式传入
要点:
-
改变
this
指向后原函数会立即执行,且此方法只是临时改变this
指向一次 -
当第一个参数为
null
、undefined
的时候,默认指向window
(在浏览器中)
call
参数:call
方法的第一个参数也是this
的指向,后面传入的是一个参数列表
其余和aplly一样
bind
参数:第一参数也是this
的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)
要点:改变this
指向后不会立即执行,而是返回一个永久改变this
指向的函数
function fn(...args){
console.log(this,args);
}
let obj = {
myname:"张三"
}
const bindFn = fn.bind(obj); // this 也会变成传入的obj ,bind不是立即执行需要执行一次
bindFn(1,2) // this指向obj
fn(1,2) // this指向window
手写实现bind
Function.prototype.myBind = function(thisObj,...argArray){
//获取调用myBind的函数
let fn = this;
//将传入的this指向转换为对象或window
thisObj = thisObj!==null && thisObj!==undefined ? Object(thisObj) : window;
//生成返回固定改变this指向的函数
function reFn(...arg){
thisObj.fn = fn;
var finalArgs = [...argArray,...arg]; //合并参数
let result = thisObj.fn(...finalArgs); //调用函数获取返回值
delete thisObj.fn
return result;
}
return reFn;
}
📕JavaScript中执行上下文和执行栈是什么
执行上下文
执行上下文是一种对Javascript
代码执行环境的抽象概念。
执行上下文分为三种类型:
-
全局执行上下文:只有一个,浏览器中的全局对象就是
window
对象,this
指向这个全局对象 -
函数执行上下文:只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文
-
Eval 函数执行上下文: 指的是运行在
eval
函数中的代码,很少用而且不建议使用
执行上下文的生命周期:
创建阶段 → 执行阶段 → 回收阶段
-
创建阶段
-
确定this的值(
This Binding
) -
创建LexicalEnvironment(词法环境) 组件
在词法环境的内部有两个组件: 环境记录器 和 一个外部环境的引用。环境记录器是存储变量和函数声明的实际位置。外部环境的引用意味着它可以访问其父级词法环境(作用域)。
词法环境有两种类型:全局环境 和 函数环境
并且,环境记录器也有两种类型:声明式环境记录器 和 对象环境记录器 ,前者用来存储函数环境内部的变量、函数和参数,后者用来定义全局环境中的变量函数。
-
创建VariableEnvironment(变量环境) 组件
变量环境也是一个词法环境。在 ES6 中,词法环境和变量环境的区别在于前者用于存储函数声明和变量(
let
和const
)绑定,而后者仅用于存储变量(var
)绑定。
这也是函数声明和var变量声明 提前的原因。
举个例子
let a = 20; const b = 30; var c; function multiply(e, f) { var g = 20; return e * f * g; } c = multiply(20, 30);
执行上下文如下:
GlobalExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { // 词法环境 EnvironmentRecord: { Type: "Object", // 标识符绑定在这里 a: < uninitialized >, b: < uninitialized >, multiply: < func > } outer: <null> }, VariableEnvironment: { // 变量环境 EnvironmentRecord: { Type: "Object", // 标识符绑定在这里 c: undefined, } outer: <null> } } FunctionExectionContext = { ThisBinding: <Global Object>, LexicalEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符绑定在这里 Arguments: {0: 20, 1: 30, length: 2}, }, outer: <GlobalLexicalEnvironment> }, VariableEnvironment: { EnvironmentRecord: { Type: "Declarative", // 标识符绑定在这里 g: undefined }, outer: <GlobalLexicalEnvironment> } }
-
-
执行阶段
在这阶段,执行变量赋值、代码执行
-
回收阶段
执行上下文出栈等待虚拟机回收执行上下文
执行栈
执行栈,也叫调用栈(Call Stack),具有 LIFO(后进先出)结构,可以把执行上下文栈认为是一个存储函数调用的栈结构。
- JavaScript 执行在单线程上,所有的代码都是排队执行。
- 一开始浏览器执行全局的代码时,首先创建全局的执行上下文,压入执行栈的顶部。
- 每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈
举个例子:
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
简单分析一下流程:
- 创建全局上下文请压入执行栈
first
函数被调用,创建函数执行上下文并压入栈- 执行
first
函数过程遇到second
函数,再创建一个函数执行上下文并压入栈 second
函数执行完毕,对应的函数执行上下文被推出执行栈,执行下一个执行上下文first
函数first
函数执行完毕,对应的函数执行上下文也被推出栈中,然后执行全局上下文- 所有代码执行完毕,全局上下文也会被推出栈中,程序结束
📕说说你对事件循环的理解
JavaScript是一种单线程非阻塞语言,而实现非阻塞的方法就是事件循环。
在JavaScript中所有的任务都可以分为:同步任务 和 异步任务,同步任务会直接进入主线程立即执行,异步任务则会放入消息队列。当主线程的任务执行完毕以后,才会执行消息队列中的任务。
除此之外,异步任务还分为 微任务 和 宏任务。
常见的宏任务有:script(整体代码), setTimeout, setInterval, setImmediate, I/O,DOM事件、Ajax等
常见的微任务有: Promise.then() .catch()(本身是同步任务),process.nextTick,MutationObserver(DOM 监听)、async/await
注意:new Promise本身是同步任务
事件循环的执行流程
-
JavaScript 代码在执行时,会将同步任务压入执行栈中,按照后进先出的顺序依次执行。
-
如果遇到异步任务,则放入任务队列中,等待执行。
-
当执行栈中的同步任务全部执行完毕后,事件循环会从任务队列中取出一个任务(按照队列的先进先出原则),将其对应的回调函数压入执行栈中执行。
-
如果在执行异步任务的回调函数时,又产生了异步任务,它们会被放入任务队列中等待执行。
-
重复以上步骤,直到任务队列中没有任务为止,事件循环终止。
-
对于微任务和宏任务,每当上一轮的宏任务完成后就会查看微任务的事件队列,将里面的微任务依次执行完,进行浏览器渲染,然后开始新的宏任务
举个例子:
var btn = document.getElementById('button')
btn.addEventListener('click', () => {
Promise.resolve().then(() => console.log(1))
console.log('listener 1')
})
btn.addEventListener('click', () => {
Promise.resolve().then(() => console.log(2))
console.log('listener 2')
})
//这里当点击事件发生后,会先触发第一个click,先输出listener 1,再输出1;
//再触发第二个click,先输出 listener 2,再输出2
btn.click();//系统触发click
//当系统发生click事件后,会同时触发两个click,这个时候就会优先执行两个同步任务,输出
//listener 1 和 listener 2
//然后依次触发两个微任务,输出 1 2
📕说说Ajax原理是什么,如何实现
Ajax是通过通过XMLHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用javascript来操作DOM而更新页面。
XMLHttpRequest是ajax的核心机制,它是在IE5中首先引入的,是一种支持异步请求的技术。从而使JavaScript可以及时向服务器提出请求和处理响应,而不阻塞用户。达到无刷新的效果。
Ajax实现过程
1. 创建Ajax核心对象XMLHttpRequest
(记得考虑兼容性)
let xhr = null;
if (window.XMLHttpRequest) {// 兼容 IE7+, Firefox, Chrome, Opera, Safari
xhr = new XMLHttpRequest();
} else {// 兼容 IE6, IE5
xhr = new ActiveXObject("Microsoft.XMLHTTP");
}
2. 传入请求方式和请求地址
xhr.open(method, url, [async][, user][, password])
参数说明:
-
method
:表示当前的请求方式,常见的有GET
、POST
-
url
:服务端地址 -
async
:布尔值,表示是否异步执行操作,默认为true
-
user
: 可选的用户名用于认证用途;默认为null
-
password
: 可选的密码用于认证用途,默认为null
3. 给服务端发送请求
通过 XMLHttpRequest
对象的 send()
方法,将客户端页面的数据发送给服务端
xhr.send([body])
body
: 在 XHR
请求中要发送的数据体,如果不传递数据则为 null
如果使用GET
请求发送数据的时候,需要注意如下:
- 将请求数据添加到
open()
方法中的url
地址中 - 发送请求数据中的
send()
方法中参数设置为null
4. 获取服务器与客户端的响应数据
可以使用onreadystatechange()
或 onload()
,区别在于前者用于监听XMLHttpRequest.readyState
属性,只要其值发生改变就触发。而后者只有处于状态码4,请求已完成,响应已就绪的情况下,才会进入onload()
需要说明的是XMLHttpRequest.readyState
属性一共有5个状态值:
0
:未初始化 — 尚未调用.open()方法;1
:启动 — 已经调用.open()方法,但尚未调用.send()方法;2
:发送 — 已经调用.send()方法,但尚未接收到响应;3
:接收 — 已经接收到部分响应数据;4
:完成 — 已经接收到全部响应数据,而且已经可以在客户端使用了;
const request = new XMLHttpRequest()
request.onreadystatechange = function(e){
if(request.readyState === 4){ // 整个请求过程完毕
if(request.status >= 200 && request.status <= 300){
console.log(request.responseText) // 服务端返回的结果
}else if(request.status >=400){
console.log("错误信息:" + request.status)
}
}
}
request.open('POST','http://xxxx')
request.send()
封装Ajax
function ajax(options){
let xhr = new XMLHttpRequest();
//处理传入参数
options = options || {};
options.type = (options.type || "GET").toUpperCase();
option.dataType = options.dataType || "json";
const params = options.data;
//发送请求
if(options.type == "GET"){
xhr.open("GET",options.url+"?"+params,true);
xhr.send(null)
}else{
xhr.open("POST",options.url,true);
xhr.send(params);
}
xhr.onreadstatechange = function(){
if(xhr.readstate===4) {
let status = xhr.status;
if(status >=200 && status<=300){
options.sucess && options.success(xhr.responseText,xhr.responseXML)
} else {
options.fail && options.fail(status)
}
}
}
}
使用方式如下
ajax({
type: 'post',
dataType: 'json',
data: {},
url: 'https://xxxx',
success: function(text,xml){//请求成功后的回调函数
console.log(text)
},
fail: function(status){请求失败后的回调函数
console.log(status)
}
})
📕面向对象与面向过程
面向过程强调的是过程,属于自顶而下的编程模式,把问题分解成一个一个步骤,逐个实现问题步骤。
面向对象强调的是对象,把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
面向对象的三大特性
-
封装:也就是把客观事物封装成抽象的类,并且可以选择隐藏或暴露属性方法
-
继承:子类继承父类,子类可以使用父类的所有暴露的功能,并在无需重新编写父类的情况下对这些功能进行扩展。
-
多态:面向对象的多态性,即“一个接口,多个方法”。多态性体现在父类中定义的属性和方法被子类继承后,可以具有不同的属性或表现方式
📕创建对象的几种方式
在ES6之前都没有正式的支持面向对象模式,比如类和继承。因此,在ES6之前都是运用原型式巧妙的模拟相应行为,因此可以通过下面几个方式创建对象,而ES6实现类与继承的原理也是封装的语法糖:
工厂模式
function createCar(color,style){
var obj = new Object();
obj.color = color;
obj.style = style;
obj.sayColor = function(){
return this.color;
}
return obj;
}
var Car1 = createCar('red','卡宴');
var Car2 = createCar('blue','911');
构造函数
虽然工厂模式解决了重复代码,但是由于创建对象返回的都是一个新的对象,因此无法判断实例对象是被哪一个工厂函数创建出来的。因此出现了构造函数的方式
function CreateCar(color,style){
this.color = color;
this.style = style;
this.sayColor = function(){
console.log(this.color);
}
}
var Car1 = new CreateCar('red','卡宴');
var Car2 = new CreateCar('blue','991');
构造函数与工厂模式的区别(显示)
- 没有显示的创建对象
- 直接将属性和方法赋值给了this对象
- 没有return语句
构造函数创建对象内部的处理
-
创建一个新对象
-
将构造函数的作用域赋给新对象
-
执行构造函数中的代码
-
返回新对象
- 如何区分数组和对象
-
为什么instanceOf可以区分数组和对象:就是因为它们采用不同的构造函数创建出来的,构造函数可以解决对象的表示问题
-
还可以通过调用对象.constructor 获取对象的构造函数,可以区分数组对象
原型模式
构造函数的主要问题在于,其定义的方法会在每个实例上都创建一遍。虽然每个实例都可以调用构造函数中定义的方法,但是每个方法不是同一个Function实例。上面构造函数中定义的方法等价于:this.sayCorlor = new Function("console.log(this.corlor)");
虽然我们可以通过将共用的方法定义在全局作用域上的方法解决这个问题,但是会污染全局环境,因此,我们引出了原型模式创建对象。
function CreateCar(corlor,style){
this.color = color;
this.style = style;
}
function CreateCar2(){
this.color = "blue";
this.style = "baoma";
}
CreateCar.prototype.configure = '顶配';
CreateCar.prototype = CreateCar2.prototype;
var Car = new CreateCar("red","aodi");
var Car2 = new CreateCar2("red","aodi");
console.log(Car.configure === Car2.configure)//true
📕说说对设计模式的理解?常见的设计模式有哪些?
设计模式是对软件设计中普遍存在的各种问题所提出的解决方案。从而增强代码的可重用性、可扩充性、 可维护性、灵活性。常见的设计模式有:单例模式
工厂模式
策略模式
代理模式
中介者模式
装饰者模式
等…
单例模式
单例模式需要保证一个类仅有一个实例,并提供一个访问它的全局访问点。
单例模式的用途同样非常广泛,比如当我们单击登录按钮的时候,页面中会出现一个登录浮窗,而这个登录浮窗是唯一的,无论单击多少次登录按钮,这个浮窗都只会被创建一次,那么这个登录浮窗就适合用单例模式来创建
- 简单实现
function Singelton(name){
this.name = name;
this.instance = null;
}
// 原型扩展类的一个方法getName()
Singleton.prototype.getName = function() {
console.log(this.name)
};
// 获取类的实例——通过这个方法创建单一实例
Singleton.getInstance = function(name) {
//如果没有创建实例
if(!this.instance) {
this.instance = new Singleton(name);
}
return this.instance
};
// 获取对象1
const a = Singleton.getInstance('a');
// 获取对象2
const b = Singleton.getInstance('b');
// 进行比较
console.log(a === b); //true
- 通过闭包实现
function Singleton(name) {
this.name = name;
}
// 原型扩展类的一个方法getName()
Singleton.prototype.getName = function() {
console.log(this.name)
};
// 获取类的实例(这里instance一直都被调用,因此一直没有被销毁)
Singleton.getInstance = (function() {
var instance = null;
return function(name) {
if(!instance) {
instance = new Singleton(name);
}
return instance
}
})();
// 获取对象1
const a = Singleton.getInstance('a');
// 获取对象2
const b = Singleton.getInstance('b');
// 进行比较
console.log(a === b); //true
工厂模式
工厂模式是用来创建对象的一种最常用的设计模式,不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。工厂模式根据抽象程度的不同可以分为:简单工厂
,工厂方法
和抽象工厂
简单工厂模式
简单工厂模式(也叫静态工厂模式),用一个工厂对象创建同一类对象类的实例
假设我们要开发一个公司岗位及其工作内容的录入信息,根据岗位的不同,工作的内容也不同,我们只需要将岗位用作函数参数传入就可以获取所需要的对象
function Factory(career) {
function User(career, work) {
this.career = career
this.work = work
}
let work
switch(career) {
case 'coder':
work = ['写代码', '修Bug']
return new User(career, work)
break
case 'hr':
work = ['招聘', '员工信息管理']
return new User(career, work)
break
case 'driver':
work = ['开车']
return new User(career, work)
break
case 'boss':
work = ['喝茶', '开会', '审批文件']
return new User(career, work)
break
default:
throw new Error('参数错误, 可选参数:coder、hr、driver、boss')
}
}
let coder = new Factory('coder')
console.log(coder)
let boss = new Factory('boss')
console.log(boss)
缺点:①每增加新的对象类型都需要修改判断逻辑代码
②并且对象类别很多的时候 ,这个工厂函数会变得十分庞大
适用:因此简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用
工厂方法模式
…未完待续…
📕原型与原型链
原型对象:当每个函数创建的时候,都会有一个prototype
属性指向一个对象,这个对象就是原型对象。
构造器:在这个原型对象上有一个custructor属性指回这个构造函数
__proto__
:当我们用这个函数创建一个实例对象的时候,实例对象身上会有一个[[prototype]]属性指向它构造函数的原型对象,而脚本中没有访问[[prototype]]的标准方法,因此,很多浏览器会暴露一个__proto__
属性可以访问对象的原型。
原型链:当我们将父类的实例赋值给子类的原型对象,我们就可以实现原型链的继承。当我们查找一个实例上的某个属性时发现实例上并没有该属性,我们就会查找该对象的原型对象,如果原型对象上也没有该属性,就会查找原型对象的原型对象上是否有该属性,直到层层向上直到一个对象的原型对象为 null
,null
没有原型。这个整个向上查找的过程就形成了一个原型链。
-
所有的构造器都是函数对象,函数对象都是
Function
构造产生的Object.__proto__ === Function.prototype
Day02
📕JavaScript如何实现继承
在ES6之前没正式支持类的定义之前,主要有6种方法实现继承:原型链继承
盗用构造函数继承
组合继承
原型式继承
寄生式继承
寄生式组合继承
原型链继承
通过将父类的实例赋值给子类的原型对象,能够让子类访问父类的所有属性和方法,从而实现继承。
function Parent(){
this.name = "曹操";
this.friends = {a:2,b:3};
}
Parent.prototype.getName = function(){
return this.name;
}
function Child(){
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
const child = new Child();
console.log(child.name);//曹操
console.log(child.getName()) //曹操
//原型继承的问题——引用数据类型
const child2 = new Child();
child2.friends.a = "666";
console.log(child.friends.a);//"666"
//但是如果这里把child2.friends整体替换掉,例如:
child2.friends = {c:2,d:3};
console.log(child.friends);//{a: '666', b: 3} child的friends属性不会改变
问题:
-
原型链继承的问题在于,所有的子类实例共享同一个原型对象,尤其是当原型包含引用值的时候,当被某一个子类实例修改时,其他子类实例也会发生变化(个人理解:为什么这里强调引用类型,而基本类型不受影响,是因为修改基本类型会直接覆盖原型上的属性,然而引用类型存在修改内容,而不整体赋值的情况(不知道对不对—>问老师))
-
子类在实例化的时候,不能在不影响所有对象实例的情况下给父类构造器传参
盗用构造函数继承
为了解决原型链继承的问题,引入了盗用构造函数继承。盗用构造函数通过在子类构造函数种使用apply()
或 call()
方法,以子类新创建实例为上下文执行父类构造函数
function Parent(name){
this.name = name;
this.colors = ["red","blue","green"];
}
function Child(){
//继承Parent并传参
Parent.call(this,"Niko");
//实例属性
this.age = 26;
}
let child = new Child();
child.colors.push("black");
conosole.log(child.colors);//"red,blue,green,black"
let child2 = new Child();
console.log(child2.colors);//"red,blue,green"
解决了实例之间共享父类属性的问题,对每一次创建的实例都会继承自己的父类属性。并且可以在子类构造函数中向父类构造函数传参
问题:子类不能访问父类原型上定义的方法,必须在构造函数中定义方法,而构造函数中定义的方法每次创建新的实例都要重新new Function()
,函数无法复用
组合继承
为了解决构造函数继承 和 原型链继承的问题,组合继承综合了两者的优点,使用原型链继承原型上的属性和方法,而通过盗用函数继承实例属性。这样既可以实现方法复用,又可以让每个子类实例从父类继承的属性方法互不影响
function Parent(name){
this.name = name;
this.friends = ["Bob","Marry","Jack"];
}
Parent.prototype.sayName = function(){
console.log(this.name);
}
//使用构造函数继承
function Child(name,age){
//解决了传参的问题
Parent.call(this,name);
this.age = age;
}
//使用原型链继承
Child.prototype = new Parent();
let child = new Child("Sarry",29);
child.friends.push("Candy");
console.log(child.friends);//["Bob","Marry","Jack","Candy"]
let child2 = new Child("Bili",22);
console.log(child2.friends);//["Bob","Marry","Jack"]
问题:会调用两次父类构造函数
原型式继承
不进行自定义类型,也能实现对象之间的信息共享。设置一个函数,将参数设置为子类的原型对象,在函数内返回一个以这个参数为原型的对象。
function object(o){
function F(){};
F.prototype = o;
return new F();
}
let person = {
name:"Nicholas",
friends:["Nihao","Court","Van"],
};
let person1 = object(person);
person1.name = "Gerg";
person1.friends.push("Rob");
let person2 = object(person);
person2.name = "Linda";
console.log(person2.friends); //"Nihao","Court","Van","Greg"
ES5中通过增加的Object.create()
也可以实现和这里object()
同样的效果:
let person = {
name:"Nicholas",
friends:["Nihao","Court","Van"],
};
let person1 = Object.create(person);
//let person1 = Object.create(person,{
// school:{
// writable: true,
// configurable: true,
// value: 'NEU',
// get: function() { ... },
// set: function(value) {
// ....
// }
// },
//});
person1.name = "Gerg";
person1.friends.push("Rob");
let person2 = Object.create(person);
person2.name = "Linda";
console.log(person2.friends); //"Nihao","Court","Van","Greg"
Object.create()
接收两个参数,第一个参数为作为新对象原型的对象,第二个可选参数为给新对象定义额外属性的对象
问题:与原型链继承一样,原型对象属性中包含的引用值会在所有实例间共享
寄生式继承
寄生式继承通过创建一个实现继承的函数,内部创建一个以传入参数为原型的对象,并以添加方法属性等方式增强这个对象,最后返回这个对象
function object(o){
function F(){};
F.prototype = o;
return new F();
}
function clone(o){
let clone = object(o);
//let clone = Object.create(o);
clone.getFriends = function() {
return this.friends;
};
return clone;
}
let parent = {
name: "parent",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
let child = clone(parent);
console.log(child.getName()); // parent
console.log(child.getFriends()); // ["p1", "p2", "p3"]
问题:原型上的引用属性所有子类实例共享,并且添加在实例身上的函数难以重用(每创建一个实例,都得执行一遍new Function())
寄生式组合继承
由于组合式继承存在效率问题,因此在寄生式组合继承时,使用寄生式继承继承父类的原型,而使用盗用构造函数的方式继承父类构造函数中的属性方法,使得子类原型取得父类原型的一个副本,避免了组合式继承中调用了两次父类构造函数,从而在子类实例和原型上重复定义了父类上所有的属性方法的问题
function clone (parent, child) {
// 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
child.prototype = Object.create(parent.prototype);
child.prototype.constructor = child;
}
function Parent() {
this.name = 'parent';
this.play = [1, 2, 3];
}
Parent.prototype.getName = function () {
return this.name;
}
function Child() {
Parent.call(this);
this.friends = 'child5';
}
clone(Parent, Child);
Child6.prototype.getFriends = function () {
return this.friends;
}
let person = new Child();
console.log(person); //{friends:"child5",name:"parent",play:[1,2,3],__proto__:Parent}
console.log(person.getName()); // parent
console.log(person.getFriends()); // child5
📕什么是防抖和节流?有什么区别?如何实现?
防抖和节流本质上是优化高频率执行代码的一种手段。例如浏览器的scroll
、mousemove
等事件触发时会不断的调用绑定在事件身上的回调函数,极大地浪费资源,降低前端性能,对此我们就可以采用 防抖(debounce) 和 节流(throttle) 的方式来减少调用频率
函数的防抖:触发事件以后,在n秒内函数只能执行一次,如果触发事件n秒以内又触发了事件,则会重新计算函数延迟执行时间
- 实现
function debounce(fn, time){
let timeout = null
return function() {
clearTimeout(timeout)
timeout = setTimeout(() => {
fn.apply(this, arguments)
}, time);
}
}
函数的节流:当持续触发事件时,保证在一定事件内只调用一次是事件处理函数,当用户小于时间间隔触发事件将不会执行回调函数
- 定时器写法
function throttle(fn,delay){
var timer;
return fuction(...args){
//这里确保在delay这段时间内,都不会触发fn函数
if(!timer){
timer = setTimeout(()=>{
fn.apply(this,args);
timer = null;
},delay)
}
}
}
- 时间戳写法
function throttle(fn,delay){
var oldTime = Data.now();
return function(...args){
let nowTime = Data.now();
if(nowTime-oldTime>=delay){
fn.apply(null,args);
oldTime = Data.now();
}
}
}
📕正则表达式
- 直接匹配
/123/
,没有加开头结尾符号,表示包含123
即可
单个字符
特殊字符 | 正则表达式 |
---|---|
换行符 | \n |
换页符 | \f |
回车符 | \r |
制表符 | \t |
垂直制表符 | \v |
回退符 | [\b] |
多个字符
- 在正则表达式里,集合的定义方式是使用中括号
[]
。如/[123]/
这个正则就能同时匹配1,2,3三个字符,只要包含其中一个即可 - 元字符
-
就可以用来表示区间范围,利用/[0-9]/
就能匹配所有的数字,/[a-z]/
则可以匹配所有的英文小写字母 —> 这个表示只要包含0~9其中一个数字都返回true - 字符集合结合边界符
/^[abc]$/
—>表示只能匹配a 或 b 或 c任意一种
匹配区间 | 正则表达式 |
---|---|
除了换行符之外的任何字符 | . |
单个数字, [0-9] (注意:这里“0”这种字符串类型数字也返回true) | \d |
除了[0-9] | \D |
包括下划线在内的单个字符,[A-Za-z0-9_] | \w |
非单字字符 | \W |
匹配空白字符,包括空格、制表符、换页符和换行符 | \s |
匹配非空白字符 | \S |
边界符
边界和标志 | 正则表达式 |
---|---|
单词边界 | \b |
非单词边界 | \B |
字符串开头 | ^ |
字符串结尾 | $ |
量词符
量词符 | 说明 |
---|---|
* | 重复0次 或 更多次 |
+ | 重复1次 或 更多次 |
? | 重复0次 或 1次 |
{n} | 重复n次 |
{n,m} | 重复n到m次 |
分组符
对字符串进行分组,小括号包裹的内容看作一个整体
var reg = /^abc{1,3}$/; //表示以ab开头,c可以出现1~3次
var reg1 = /^(abc){1,3}$/;//表示`abc`整体可以出现1~3次
正则表达式的参数
- 修饰符可以组合使用:
/a/gi
(表示全局匹配+不区分大小写)
var reg = /表达式/[参数]
表示 | 说明 |
---|---|
g | 全局匹配(字符串内所有能匹配到的都会返回) |
i | 不区分大小写搜索 |
m | 多行搜索 |
s | 允许“.”匹配换行符 |
u | 使用unicode码的模式进行匹配 |
y | 执行”粘性“搜索 |
正则表达式常见的用法
replace替换
第一个参数:可以是一个正则也可以是一个字符串
第二个参数:需要替换的内容,也可以是一个函数(函数第一个参数就是正则能匹配到的内容)
//手机号中间四位替换成*
var rg = /(\d{3})\d{4}(\d{4})/;
var str = '13899992929';
str.replace(/(\d{3})\d{4}(\d{4})/,"$1****$2");//$1表示分组符的第一个分组,表示第一个分组+'****'+第二个分组
//将句子 my name is mjn,nice to meet you的所有首字母大写
var reg = /\b(\w)/g;//表示匹配所有字符边界
var str1 = 'my name is mjn,nice to meet you';
str1.replace(reg,fucntion(item){
return item.toUpperCase();
})
console.log(str1);//My Name Is Mjn,Nice To Meet You
ES6部分
📕说说let、const、var之间的区别
var关键字
-
var在全局作用域中声明的变量会成为window对象的属性
var a = 10; console.log(window.a) // 10
-
var声明的变量存在变量提升
console.log(a) // undefined var a = 20 //在编译阶段,编译器会将其变成以下执行 var a console.log(a) a = 20
-
使用
var
,我们能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声明var a = 20 var a = 30 console.log(a) // 30
let 关键字
-
let 声明的范围是块级作用域,而var声明的范围是函数作用域
if(true){ let name = "Matt"; console.log(name); //Matt } console.log(name); //ReferenceError name没有定义
-
let不存在声明提前,因此,let变量声明之前该变量都是不可用的,let声明之前的执行瞬间被称为“暂时性死区”
console.log(age);//age没有dingyi let age = 25
-
let
不允许在相同作用域中重复声明,并且,不能在函数内部重新声明参数let a = 20 let a = 30 // Uncaught SyntaxError: Identifier 'a' has already been declared function func(arg) { let arg; } func() // Uncaught SyntaxError: Identifier 'arg' has already been declared
-
使用let在全局作用域中声明的变量不会成为window对象的属性(var会)
let age = 25; console.log(window.age);//undefined
const关键字
-
const
声明一个只读的常量,一旦声明,常量的值就不能改变。因此,const
一旦声明变量,就必须立即初始化const a = 1 a = 3 // TypeError: Assignment to constant variable.
-
内存地址的引用是不可改变的,但是比如修改对象内部的属性值,这是可以的
const foo = {}; foo.prop = 123;// 为 foo 添加一个属性,可以成功 foo.prop; // 123 // 将 foo 指向另一个对象,就会报错 foo = {}; // TypeError: "foo" is read-only
扩展:如果真的想要不能修改对象的属性值,可以使用
Object.freeze()
const foo = Object.freeze({}); // 常规模式时,下面一行不起作用 foo.prop = 123; console.log(foo); //里面没有prop属性 // 严格模式时,该行会报错 'use strict'; const foo = Object.freeze({}); foo.prop = 123; // 报错
~~其余特性和let基本相同
为什么要引入块级作用域
没有引入块级作用域的时候,函数内部代码块内的变量会自动提升,就会出现以下情况:
-
内层变量覆盖外层变量
var tmp = new Date(); function f() { console.log(tmp);//外层的tmp被内部的提前声明覆盖了 if (false) { var tmp = 'hello world'; }} f(); // undefined
-
用来计数的循环变量泄露为全局变量
注意:对于for的块级作用域,设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域
var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); };} a[6](); // 10 //相当于 var a = []; var i; for (; i < 10; i++) { a[i] = function () { console.log(i); };} a[6](); // 10 //使用let修改 var a = [];for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); };} a[6](); // 6
变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6
📕ES6数组新增的扩展
扩展运算符的应用
解构赋值
原来我们需要提取数组中的某一个值,需要以arr[0]
这种数组索引的方式获取。然而ES6 允许解构赋值,只要等号两边的模式相同,左边的变量就会被赋予右边对应的值
- 如果解构不成功,变量的值就等于undefined
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
扩展运算符
扩展运算符可以将一个数组转为用逗号分隔的参数序列
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
扩展运算符可以与解构赋值结合起来,用于生成数组
- 注意:将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
const [first, ...rest] = ["foo"];
first // "foo"
rest // []
//放在最后一项
const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错
const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错
定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
const obj = {a: 1, b: 2};
let arr = [...obj]; // TypeError: Cannot spread non-iterable object
注意:通过扩展运算符实现的是浅拷贝,修改了引用指向的值,会同步反映到新数组
const arr1 = ['a', 'b',[1,2]];
const arr2 = ['c'];
const arr3 = [...arr1,...arr2]
arr[1][0] = 9999 // 修改arr1里面数组成员值
console.log(arr[3]) // 影响到arr3,['a','b',[9999,2],'c']
Array构造函数新增方法
关于构造函数,数组新增的方法有:Array.from()
Array.of()
Array.from()
可将 类似数组的对象 和 可遍历(iterable)
的对象(包括 ES6
新增的数据结构 Set
和 Map
) 转为真正的数组
参数:第一个参数为需要转换的对象,第二个可选参数可以对每个元素进行处理,将处理后的值放入返回的数组
//类数组对象
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
//第二参数使用
Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]
Array.of()
用于将一组值,转换为数组
Array.of(3, 11, 8) // [3,11,8]
参数:没有参数的时候,返回一个空数组;当参数只有一个的时候,实际上是指定数组的长度;参数个数不少于 2 个时,Array()
才会返回由参数组成的新数组
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
实例对象上新增的方法
迭代器方法
ES6中暴露了3个用于检索数组内容的方法:keys()
values()
entries()
keys()
是对键名的遍历、values()
是对键值的遍历,entries()
是对键值对的遍历
const a = ["foo","bar","baz"];
//这些方法都返回迭代器,所以可以通过Array.from()直接转换为数组实例
const aKeys = Array.from(a.keys());
const aValues = Array.from(a.values());
const aEntries = Array.from(a.entries());
console.log(aKeys);//[0,1,2,3]
console.log(aValues);//["foo","bar","baz"]
console.log(aEntries);//[0,"foo"],[1,"bar"],[2,"baz"]
//或者
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
复制和填充
批量复制方法:copyWithin()
以及 填充数组方法:fill()
copyWithin()
将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组
参数:
- target(必需):从该位置开始替换数据。如果为负值,表示倒数。
- start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
- end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
[1, 2, 3, 4, 5].copyWithin(0, 3) // 将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2
// [4, 5, 3, 4, 5]
fill()
使用给定值,填充一个数组
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
注意,如果填充的类型为对象,则是浅拷贝(copyWithin也是)
搜索方法
搜索方法分为:严格相等搜索 和 断言函数搜索
-
严格相等搜索
includes()
用于判断数组是否包含给定的值参数:方法的第二个参数表示搜索的起始位置,默认为
0
。参数为负数则表示倒数的位置[1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // false [1, 2, NaN].includes(NaN) // true
-
断言函数搜索
find()、findIndex()
find()
用于找出第一个符合条件的数组成员参数是一个回调函数,接受三个参数依次为当前的值、当前的位置和原数组。这两个方法都可以接受第二个参数,用来绑定回调函数的
this
对象。[1, 5, 10, 15].find(function(value, index, arr) { return value > 9; }) // 10 //绑定回调函数的this对象 function f(v){ return v > this.age; } let person = {name: 'John', age: 20}; [10, 12, 26, 15].find(f, person); // 26
findIndex()
返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1[1, 5, 10, 15].findIndex(function(value, index, arr) { return value > 9; }) // 2
扁平化方法
flat()
将数组扁平化处理,返回一个新数组,对原数据没有影响
[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]
flat()
默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()
方法的参数写成一个整数,表示想要拉平的层数
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
flatMap()
flatMap()
方法对原数组的每个成员执行一个函数相当于执行Array.prototype.map()
,然后对返回值组成的数组执行flat()
方法。该方法返回一个新数组,不改变原数组
flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
数组空位
ES6之前的方法会忽略空位,然而在ES6之后新增的方法普遍将空位当成存在的元素,值为undefined
其中包括Array.from
、扩展运算符、解构赋值、copyWithin()
、fill()
、entries()
、keys()
、values()
、find()
和findIndex()
但是由于标准不统一不建议使用
📕ES6对象新增了哪些扩展
属性的简写
ES6中,当对象键名与对应值名相等的时候,可以进行简写
const baz = {foo:foo}
// 等同于
const baz = {foo}
注意:简写的对象方法不能用作构造函数,否则会报错
const obj = {
f() {
this.foo = 'bar';
}
};
属性名表达式
ES6 允许字面量定义对象时,将表达式放在括号内,也可以定义方法名
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
//定义方法名
let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi
注意,属性名表达式与简洁表示法,不能同时使用,会报错
// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };
// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};
注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object]
const keyA = {a: 1};
const keyB = {b: 2};
const myObject = {
[keyA]: 'valueA',
[keyB]: 'valueB'
};
myObject // Object {[object Object]: "valueB"}
对象的解构赋值
数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
// 注意:如果解构失败,变量的值等于undefined。
let {foo} = {bar: 'baz'};
foo // undefined
解构赋值的内部机制:先找到 属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
foo // error: foo is not defined
要点:
- 与数组一样,解构也可以用于嵌套结构的对象
let obj = {
p: [
'Hello',
{ y: 'World' }
]};
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
- 对象的解构赋值可以取到继承的属性。
const obj1 = {};
const obj2 = { foo: 'bar' };
// Object.setPrototypeOf(obj1, obj2);
// obj1.__proto__ = obj2
const { foo } = obj1;
console.log(foo);
- 对象的解构也可以指定默认值(默认值生效的条件是,对象的属性值严格等于undefined)
var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
// 默认值生效的条件是,对象的属性值严格等于undefined。
var {x = 3} = {x: undefined};x // 3
var {x = 3} = {x: null};
x // null
// 上面代码中,属性x等于null,因为null与undefined不严格相等,所以是个有效的赋值,导致默认值3不会生效。
- 解构赋值对于引用类型数据的赋值时是浅拷贝
let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2; // 修改obj里面a属性中键值
x.a.b // 2,影响到了结构出来x的值
属性的遍历
ES6 一共有 5 种方法可以遍历对象的属性。
for...in
:循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)Object.keys(obj)
:返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名Object.getOwnPropertyNames(obj)
:回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名Object.getOwnPropertySymbols(obj)
:返回一个数组,包含对象自身的所有 Symbol 属性的键名Reflect.ownKeys(obj)
:返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举
对象新增的方法
关于对象新增的方法,分别有以下:
- Object.is()
- Object.assign()
- Object.getOwnPropertyDescriptors()
- Object.setPrototypeOf(),Object.getPrototypeOf()
- Object.keys(),Object.values(),Object.entries()
- Object.fromEntries()
Object.is()
严格判断两个值是否相等,与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是+0
不等于-0
,二是NaN
等于自身
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
Object.assign()
Object.assign()
方法用于对象的合并,将源对象source
的所有可枚举属性,复制到目标对象target
参数:Object.assign()
方法的第一个参数是目标对象,后面的参数都是源对象
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
注意:Object.assign()
方法是浅拷贝,遇到同名属性会进行替换
Object.getOwnPropertyDescriptors()
返回指定对象所有自身属性(非继承属性)的描述对象
const obj = {
foo: 123,
get bar() { return 'abc' }
};
Object.getOwnPropertyDescriptors(obj)
// { foo:
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true },
// bar:
// { get: [Function: get bar],
// set: undefined,
// enumerable: true,
// configurable: true }
// }
Object.setPrototypeOf()
Object.setPrototypeOf
方法用来设置一个对象的原型对象
Object.setPrototypeOf(object, prototype)
// 用法
const o = Object.setPrototypeOf({}, null);
Object.getPrototypeOf()
用于读取一个对象的原型对象
Object.getPrototypeOf(obj);
Object.keys()
返回自身的(不含继承的)所有可遍历(enumerable)属性的键名的数组
var obj = { foo: 'bar', baz: 42 };
Object.keys(obj)
// ["foo", "baz"]
Object.values()
返回自身的(不含继承的)所有可遍历(enumerable)属性的键对应值的数组
const obj = { foo: 'bar', baz: 42 };
Object.values(obj)
// ["bar", 42]
Object.entries()
返回一个对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对的数组
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
Object.fromEntries()
用于将一个键值对数组转为对象
Object.fromEntries([
['foo', 'bar'],
['baz', 42]
])
// { foo: "bar", baz: 42 }
📕如何理解ES6新增的Symbol数据结构
ES6 引入了的一种新的原始数据类型Symbol,可以用于创建对象的唯一标识符,可以接受一个字符串作为参数,表示对 Symbol 实例的描述。
原始对象的问题:当用两个不同的对象作为另一个对象的键值时,后者会覆盖前者
let a = {a:1}
let b = {b:1}
let c = {}
c[a] = 3
c[b] = 4
console.log(c); // [object Object]: 4
//遇到对象键名都会调用String() 或 toString()方法 ——> 就转换成了[object Object]
解决方案:因此可以使用Symbol()
来解决这个问题
let obj = {a:2,b:3}
let obj1 = {a:3,b:4}
let s = Symbol(obj);
let s1 = Symbol(obj1);
let o = {}
o[s] = 1;
o[s1] = 2;
console.log(o); //{Symbol([object Object]): 1, Symbol([object Object]): 2}
要点:
-
Symbol() 函数不能与 new 关键字一起作为构造函数使用
-
每次通过Symbol()函数创建的Symbol都是唯一的,相同参数的Symbol函数的返回值是不相等
let s1 = Symbol('foo'); let s2 = Symbol('foo'); s1 === s2 // false
-
Symbol 值不能与其他类型的值进行运算,会报错。
let sym = Symbol('My symbol'); "your symbol is " + sym // TypeError: can't convert symbol to string`your symbol is ${sym}` // TypeError: can't convert symbol to string
-
Symbol 值可以显式转为字符串。
let sym = Symbol('My symbol'); String(sym) // 'Symbol(My symbol)' sym.toString() // 'Symbol(My symbol)'
-
Symbol 值也可以转为布尔值,但是不能转为数值。
let sym = Symbol(); Boolean(sym) // true !sym // false Number(sym) // TypeErrorsym + 2 // TypeError
-
使用
Symbol.for()
方法和Symbol.keyFor()
方法从全局的 symbol 注册表设置和取得 symbol
常用:
-
可以使用
Symbol()
作为对象的属性名,不会覆盖原有同名属性(创建对象的唯一标识符) -
由于枚举不会列出
Symbol()
属性,因此可以用Symbol()
定义私有属性// 私有属性 let private = Symbol('private') var obj = { _name:'张三', [private]:'私有的属性', say:function(){ console.log(this[private]) } } console.log(Object.keys(obj)) //['_name', 'say']
📕如何理解ES6新增的Set、Map两种数据结构
Set
Set
是ES6引入的引用型数据结构,它的成员的值都是唯一的,Set
本质是一个构造函数,称为集合。Set函数可以接受一个数组作为参数,用来初始化
判断Set数据类型
let set = new Set([1,2,3,4])
//方法一
let res = set instanceof Set
//方法二
let resSet = Object.prototype.toString.call(set)
console.log(res); // true
console.log(resSet); // [object Set]
注:这两种方法也可以区分数组
和 对象
Set内部判断值的机制
Set 内部判断两个值是否不同,使用的算法它类似于精确相等运算符(===)
- 特殊:虽然
NaN === NaN
返回false,但是在Set数据内部认为相等
let set = new Set();
let a = 5;
let b = '5';
set.add(a);
set.add(b);
console.log(Array.from(set))//[5, '5']
//特殊情况
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);set // Set {NaN}
Set常用操作
-
与扩展运算符相结合实现数组或字符串去重
// 数组 let arr = [3, 5, 2, 2, 5, 5]; let unique = [...new Set(arr)]; // [3, 5, 2] // 字符串 let str = "352255"; let unique = [...new Set(str)].join(""); // "352"
-
实现并集、交集、和差集
let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); // 并集 let union = new Set([...a, ...b]); // Set {1, 2, 3, 4} // 交集 let intersect = new Set([...a].filter(x => b.has(x))); // set {2, 3} // (a 相对于 b 的)差集 let difference = new Set([...a].filter(x => !b.has(x))); // Set {1}
Set实例的属性和方法
属性:size()
方法:Set的实例关于增删改查的方法
- add()
- delete()
- has()
- clear()
遍历:
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回键值对的遍历器
- forEach():使用回调函数遍历每个成员
Map
Map()
类似于对象,但是“键”的范围不限于字符串。也可以使用判断Set
类型的方法判断Map
类型。
参数:可以接受一个数组作为参数。注意该数组的成员是一个个表示键值对的数组
Map
结构也可以解决上述对象中对象类型键名被覆盖的问题(对象类型键值对被覆盖)
let o1 = {a:1}
let o2 = {b:1}
let o3 = new Map()
o3.set(o1,'123')
o3.set(o2,'234')
console.log(o3);
console.log(o3.get(o1)); //"123"
console.log(o3.get(o2)); //"234"
- 接受数组作为参数
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.get('name') // "张三"
- 传址特点–对象作为键名,传输的是地址
let m = new Map([
[123,'abc'],
[{x:1},'cdf'],]);
console.log(m.get({x:1}));-->undefined
// 可以修改成下面形式
let obj = {x:1};
let m = new Map([
[123,'abc'],
[obj,'cdf'],]);
console.log(m.get(obj));-->cdf
属性和方法:
Map
结构的实例针对增删改查有以下属性和操作方法:
- size 属性
- set()
- get()
- has()
- delete()
- clear()
Map
结构原生提供三个遍历器生成函数和一个遍历方法:
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回所有成员的遍历器
- forEach():遍历 Map 的所有成员
注意:Set 和 Map数据结构都具有 Iterable
接口
WeakSet 和 WeakMap
WeakSet
WeakSet
可以接受一个具有 Iterable
接口的对象作为参数
WeakSet
与Set
的区别:
WeakSet
没有遍历操作的API
WeakSet
没有size
属性WeakSet
成员只能是引用类型,而不能是其他类型的值
let ws=new WeakSet();
// 成员不是引用类型
let weakSet=new WeakSet([2,3]);
console.log(weakSet) // 报错
// 成员为引用类型
let obj1={name:1}
let obj2={name:1}
let ws=new WeakSet([obj1,obj2]);
console.log(ws) //WeakSet {{…}, {…}}
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}
WeakSet里面的引用只要在外部消失,它在 WeakSet里面的引用就会自动消失
总而言之,就是WeakSet引用了外部的值,当这个值被垃圾回收清理以后(外部消失),WeakSet内部的这个值的引用也会自动消失
const ws = new WeakSet();
ws.add({});
//add()方法初始化了一个新对象,并将它用作ws的一个值。因为没有指向这个对象的其他引用,所以当这行代码执行完成以后,这个对象值就会被当做垃圾回收,这个值就从弱集合中消失了,使ws成为一个空集合
const ws = new WeakSet();
const container = {
val:{}
};
ws.add(container.val);
function removeReference(){
container.val = null;
}
//container 维护着一个对弱集合的引用,因此这个对象不会成为垃圾回收的目标。不过调用了removeReference方法以后,就会摧毁值对象的最后一个引用,垃圾回收就会把这个值清理掉。从而WeakSet里面也没有这个值了
因此,WeakSet
没有迭代 和 clear方法
WeakMap
WeakMap
与Map
有两个区别:
-
没有遍历操作的
API
-
没有
clear
清空方法 -
WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名const map = new WeakMap(); map.set(1, 2) // TypeError: 1 is not an object! map.set(Symbol(), 2) // TypeError: Invalid value used as weak map key map.set(null, 2) // TypeError: Invalid value used as weak map key
注意:WeakMap
的键名所指向的对象,一旦不再需要(没有对这个对象的引用,被垃圾回收了),里面的键名对象和所对应的键值对会自动消失,不用手动删除引用
const wm = new WeakMap();
const container = {
key:{}
};
wm.set(container.key,"val");
function removeReference(){
container.key = null;
}
//当removeReference被调用以后,摧毁了该对象最后一个引用,因此垃圾回收就会把这对键-值清理掉
注意:键值obj
会在WeakMap
产生新的引用,当你修改obj
不会影响到内部
const wm = new WeakMap();
let key = {};
let obj = {foo: 1};
wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}
使用场景
在网页的 DOM 元素上添加数据,就可以使用WeakMap
结构,当该 DOM 元素被清除,其所对应的WeakMap
记录就会自动被移除
const wm = new WeakMap();
const element = document.getElementById('example');
wm.set(element, 'some information');
wm.get(element) // "some information"
📕箭头函数 与 普通函数的区别
- this指向的问题:箭头函数本身是没有this的,它的this是从他作用域链的上一层继承来的,并且无法通过call和apply改变this指向
// 箭头函数this继承作用域上一层
var fn = function () {
return () => { console.log(this.name) }
}
var obj1 = {
name: '张三'
}
var obj2 = {
name: '李四'
}
var name = '王五'
obj1.fn = fn
obj2.fn = fn
obj1.fn()()//张三
obj2.fn()()//李四
fn()()//王五
// 箭头函数不能通过call改变this
var user = {
name: '张三',
fn: function () {
var obj = {
name: '李四'
}
var f = () => this.name
return f.call(obj)
}
}
- 不能作为构造函数 没有prototype属性
var fn = ()=>{};
new fn(); //Fn is not a constructor at
console.log(fn.prototype,"prototype");//没有prototype属性
- 没有arguments对象
var foo = ()=>{
console.log(arguments); //argument is not defined
}
foo(1,2,3)
//可以通过res参数这种方式获取
var foo = (...arguments){
console.log(arguments)
}
- 不能使用yield命令,因此箭头函数不能用作 Generator 函数
var fn = *()=>{}//这种写法就不支持
📕ES6中函数新增了哪些扩展
参数
-
ES6
允许为函数的参数设置默认值function log(x, y = 'World') { console.log(x, y); } console.log('Hello') // Hello World console.log('Hello', 'China') // Hello China console.log('Hello', '') // Hello //一道练习题 //这里的流程顺序是:①如果没有参数,先进行参数的赋值操作 ②这里{x=0,y=0}是相当于解构赋值,当赋值操作进行完以后,进行这里的结构赋值操作 function m1({x = 0, y = 0} = {}) { return [x, y]; } function m2({x, y} = { x: 0, y: 0 }) { return [x, y]; } //这里的流程为:调用m1,传参为空,因此赋值{},随后对{}进行解构赋值,解构{}中x为0,y为0 console.log(m1()) //[0,0] console.log(m2()) //[0,0] console.log(m1({x: 3})) //[3,0] console.log(m2({x: 3})) //[3,undefined]
-
通过rest参数获取函数的多余参数
function fn (x, ...y) { console.log(x) console.log(y) } fn(1, 2, 3, 4)
属性
函数的length属性
length
将返回没有指定默认值的参数个数
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
rest
参数也不会计入length
属性
(function(...args) {}).length // 0
- 如果设置了默认值的参数不是尾参数,那么
length
属性也不再计入后面的参数了
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
函数的name属性
- 返回该函数的函数名
var f = function () {};
// ES6
f.name // "f"
- 如果将一个具名函数赋值给一个变量,则
name
属性都返回这个具名函数原本的名字
const bar = function baz() {};
bar.name // "baz"
Function
构造函数返回的函数实例,name
属性的值为anonymous
(new Function).name // "anonymous"
bind
返回的函数,name
属性值会加上bound
前缀
function foo() {};
foo.bind({}).name // "bound foo"
注意:
- 一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域等到初始化结束,这个作用域就会消失。在不设置参数默认值时,是不会出现的
- 只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式
📕如何理解ES6中的promise
Promise 是异步编程的一种解决方案(传统的异步编程嵌套问题可能造成回调地狱,promise可以解决这样的问题)
状态
promise
对象仅有三种状态:
pending
(进行中)fulfilled
(已成功)rejected
(已失败)
// pending
new Promise((resolve, reject) => {})
// fulfilled
new Promise((resolve, reject) => { resolve('hello world') })
// rejected
new Promise((resolve, reject) => { reject('bad code') })
特点
-
对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态
-
一旦状态改变(从
pending
变为fulfilled
和从pending
变为rejected
),就不会再变,任何时候都可以得到这个结果
new Promise((resolve, reject) => {
reject('bad code')
resolve('hello world')
}).then(val => {
console.log(val)
}).catch(err => {
console.log(err)
})
promise相关方法
Promise.resolve()
Promise.resolve()方法会返回一个状态为fulfilled的promise对象。
Promise.resolve(2).then((val) => {
console.log(val)
})
Promise.reject()
Promise.reject()方法返回一个带有拒绝原因的Promise对象。
Promise.reject({ message: '接口返回错误' }).catch((err) => {
console.log(err)
})
实例方法
then()
then
是实例状态发生改变时的回调函数,第一个参数是resolved
状态的回调函数,第二个参数是rejected
状态的回调函数
then
方法返回的是一个新的Promise
实例,也就是promise
能链式书写的原因
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
catch()
catch()
方法是.then(null, rejection)
的别名,本质是一个语法糖,用于指定发生错误时的回调函数
Promise
对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止
getJSON('/posts.json').then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
Promise
对象抛出的错误不会传递到外层代码,即不会有任何反应
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
//浏览器运行到这一行,会打印出错误提示`ReferenceError: x is not defined`,但是不会退出进程catch()方法之中,还能再抛出错误,通过后面`catch`方法捕获到
finally()
finally()
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
.carch()
方法 和 .then()
中第二个参数的运行规则:
主要区别:如果在then的第一个函数里抛出了异常,后面的catch能捕获到,而then的第二个函数捕获不到
then的第二个参数和catch捕获错误信息的时候会就近原则,如果是promise内部报错,reject抛出错误后,then的第二个参数和catch方法都存在的情况下,只有then的第二个参数能捕获到,如果then的第二个参数不存在,则catch方法会捕获到。
//此时只有catch可以捕获到.then内部抛出的错误信息
let promise = new Promise((resolve,reject)=>resolve("nihao"));
promise.then(res => {
throw new Error('hello');
}, err => {
console.log("err:"+err);
}).catch(err1 => {
console.log("err1:"err1);
});
//此时只有then的第二个参数可以捕获到错误信息
const promise = new Promise((resolve, rejected) => {
throw new Error('test');
});
promise.then(res => {
//
}, err => {
console.log(err);
}).catch(err1 => {
console.log(err1);
});
//此时catch可以捕获到Promise内部抛出的错误信息
promise.then(res => {
throw new Error('hello');
}).catch(err1 => {
console.log(err1);
});
构造函数方法
Promise
构造函数存在以下方法:all()
race()
any()
allSettled()
resolve()
reject()
try()
all()
Promise.all()
方法用于将多个 Promise
实例,包装成一个新的 Promise
实例()
const p = Promise.all([p1, p2, p3]);
接受一个数组(迭代对象)作为参数,数组成员都应为Promise
实例
实例p
的状态由p1
、p2
、p3
决定,分为两种:
- 只有
p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数(这个时候会等待所有promise状态进行变更以后返回) - 只要
p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数
注意:如果作为参数的 Promise
实例,定义了catch
方法,那么它一旦被rejected
,并不会触发Promise.all()
的catch
方法
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result));
.catch(e => console.log(e));
// ["hello", Error: 报错了]
如果p2
没有自己的catch
方法,就会调用Promise.all()
的catch
方法
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// Error: 报错了
race()
Promise.race()
同样是将多个promise
实例包装成新的promise
实例。
只要参数promise
实例数组之中有一个实例率先改变状态,p
的状态就跟着改变。率先改变的 Promise 实例的返回值则传递给p
的回调函数
//race()格式
const p = Promise.race([p1, p2, p3]);
//实际运用—超时请求
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p
.then(result => console.log(result))
.catch(console.error);
any()
该方法接受一组 Promise 实例作为参数,只要参数实例有一个变成fulfilled
状态,包装实例就会变成fulfilled
状态;如果所有参数实例都变成rejected
状态,包装实例就会变成rejected
状态
注意:Promise.any()
跟Promise.race()
的区别在于—Promise.any()
不会因为某个 Promise 变成rejected
状态而结束,必须等到所有参数 Promise 变成rejected
状态才会结束
const promises = [
fetch('/endpoint-a').then(() => 'a'),
fetch('/endpoint-b').then(() => 'b'),
fetch('/endpoint-c').then(() => 'c'),
];
try {
const first = await Promise.any(promises);
console.log(first);
} catch (error) {
console.log(error);
}
allSettled()
Promise.allSettled()
方法接受一组 Promise
实例作为参数,包装成一个新的 Promise 实例
只有等到所有这些参数实例都返回结果,不管是fulfilled
还是rejected
,包装实例才会结束
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
removeLoadingIndicator();
Promise的运行机制
function fn () {
//3. 这里new Promise属于同步任务,因此会先打印 Promise1
return new Promise((resolve) => {
console.log('Promise1');
//4. 调用fn1()
fn1();
//8. fn1()执行完毕以后,将setTimeout加入宏任务消息队列(由于上一个宏任务在队列前,因此先执行)
setTimeout(() => {
//11. 执行这个宏任务,输出promise2
console.log('Promise2')
//12. 改变fn()返回的promise状态
resolve()
//13. 输出promise3
console.log('Promise3')
}, 0);
})
}
async function fn1() {
//5. promise异步任务——微任务 加入微任务消息队列
var p = Promise.resolve().then(() => {
console.log('Promise6')
})
//6. 遇到await等待await代码执行完毕,才会继续执行之后代码
//由于p.then()必须等待p状态变化,因此会执行微任务消息队列中【5】,输出Promise6
//然后执行这里的.then(),输出Promise7
await p.then(() => {
console.log('Promise7')
})
//7. await 执行完毕以后输出“end”
console.log('end')
}
//1. 同步任务,打印“script”
console.log('script')
//异步任务—宏任务 加入宏任务消息队列
setTimeout(() => {
//10. 执行这个宏任务,输出setTimeout
console.log('setTimeout')
}, 0)
//2. 这里调用fn(),等待fn()返回promise状态改变以后,再调用.then()
fn().then(() => {
//9. 由于这里需要fn()改变状态以后才能触发,因此先执行宏任务
//14. 输出Promise4
console.log('Promise4')
})
//script
//Promise1
//Promise6
//Promise7
//end
//setTimeout
//Promise2
//Promise3
//Promise4
📕Promise实现原理
基础版 Promise
适用于Primise()
中没有异步操作的简单使用方法:
// 三个状态:PENDING、FULFILLED、REJECTED
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
class Promise {
constructor(executor){
//定义状态
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
//成功的回调
let resolve = (value) => {
if(this.status===PENDING){
this.status = FULFILLED;
this.value = value;
}
}
//失败回调
let reject = (reason) => {
if(this.status===PENDING){
this.status = REJECTED;
this.reason = reason;
}
}
try{
executor(resolve,reject);
}catch(error){
reject(error);
}
}
then(onFullfilled,onReject){
if(this.status===FULFILLED){
onFullfilled(this.value);
}
if(this.status === REJECTED){
onReject(this.reason);
}
}
}
但如果我们在 executor 中使用 setTimeout 延迟执行 resolve 或 reject,我们会发现执行 then 时,当前状态为 pending,因此我们还需要加入 pending 状态下的判断
// 三个状态:PENDING、FULFILLED、REJECTED
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
class Promise {
constructor(executor){
//定义状态
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onFullfilledCallbacks = [];;
this.onRejectedCallbacks = [];
//成功的回调
let resolve = (value) => {
if(this.status===PENDING){
this.status = FULFILLED;
this.value = value;
this.onFullfilledCallbacks.forEach(fn=>fn());
}
}
//失败回调
let reject = (reason) => {
if(this.status===PENDING){
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(fn=>fn());
}
}
try{
executor(resolve,reject);
}catch(error){
reject(error);
}
}
then(onFullfilled,onReject){
if(this.status===FULFILLED){
onFullfilled(this.value);
}
if(this.status === REJECTED){
onReject(this.reason);
}
if(this.status === PENDING){
//这里当遇到异步任务的时候会出现先进入.then()函数并且状态为:PENDING的状态
//因此如果想要解决异步的问题,就需要将回调函数放入回调函数数组中储存下来,等到promise的状态改变以后执行
this.onFullfilledCallbacks.push(()=>{onFullfilled(this.value)});
this.onRejectedCallbacks.push(()=>{onReject(this.reason)});
}
}
}
let promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('成功');
},1000);
}).then(
(data) => {
console.log('success', data)
},
(err) => {
console.log('faild', err)
}
)
then 的链式调用&值穿透特性
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
const resolvePromise = (promise2, x, resolve, reject) => {
// 自己等待自己完成是错误的实现
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
}
// 标识符,为了让resolve 或 reject 只执行一次
let called;
//情况一:x为对象(promise) 或 函数
if (x && (typeof x === 'object' || typeof x === 'function')) {
try {
//取出x的then方法的时候,为了防止then抛出错误,需要try...catch【可以假设是一个promise实例】
let then = x.then;
if (typeof then === 'function') {
//调用x的.then方法,.then方法可以根据x的状态去决定resolve还是reject
then.call(x, y => {
if (called) return;
called = true;
// 递归解析的过程(因为可能 promise 中还有 promise)
resolvePromise(promise2, y, resolve, reject);
}, r => {
// 如果之前y=>{}这个函数执行了,那么这边和之后的就不会执行,因为called==true
if (called) return;
called = true;
reject(r);
});
} else {
// 如果 x.then 是个普通值就直接返回 resolve 作为结果
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e)
}
} else {
// 如果 x 是个普通值就直接返回 resolve 作为结果 Promise/A+ 2.3.4
resolve(x)
}
}
class Promise {
constructor(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks= [];
let resolve = (value) => {
if(this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.onResolvedCallbacks.forEach(fn=>fn());
}
}
let reject = (reason) => {
if(this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.onRejectedCallbacks.forEach(fn=>fn());
}
}
try {
executor(resolve,reject)
} catch (error) {
reject(error)
}
}
then(onFulfilled, onRejected) {
//解决 onFufilled,onRejected 没有传值的问题
//Promise/A+ 2.2.1 / Promise/A+ 2.2.5 / Promise/A+ 2.2.7.3 / Promise/A+ 2.2.7.4
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
//因为错误的值要让后面访问到,所以这里也要跑出个错误,不然会在之后 then 的 resolve 中捕获
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
// 每次调用 then 都返回一个新的 promise Promise/A+ 2.2.7
let promise2 = new Promise((resolve, reject) => {
if (this.status === FULFILLED) {
//为了是西安promise异步,因此加了setTimeout
setTimeout(() => {
try {
let x = onFulfilled(this.value);
// x可能是一个proimise
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e)
}
}, 0);
}
if (this.status === REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e)
}
}, 0);
}
if (this.status === PENDING) {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e)
}
}, 0);
});
this.onRejectedCallbacks.push(()=> {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0);
});
}
});
return promise2;
}
}
Promie实例方法
Promise.prototype.catch()
function catch(callback){
return this.then(null,callback);
}
Promise.prototype.finally()
finally(fn) {
return this.then((value) => {
return Promise.resolve(fn()).then(() => value)
}, (err) => {
return Promise.resolve(fn()).then(() => { throw err })
})
}
Promise类方法
Promise.resolve()
Promise.resolve = function(data) {
return new Promise((resolve) => {
resolve(data)
})
}
Promise.reject()
Promise.reject = function(data) {
return new Promise((resolve, reject) => {
reject(data)
})
}
Promise.all()
Promise.all = funciton(iterator){
//是否是可迭代对象——itertor是对象或字符串,并且iterator[Symbol].iterator 类型是一个函数
const isIterable = ((typeof iterator === "object" && iterator !== null) || typeof iterator === "string") && typeof iterator[Symbol.iterator] === "function";
//是可迭代参数
if (isIterable) {
let resolveCount = 0;
const promiseResult = [];
//将iterator转换为数组
iterator = Array.from(iterator);
return new Promise((resolve, reject) => {
//iterator为空返回[]
if (!iterator.length) {
resolve([]);
}
iterator.forEach((promise, index) => {
//将iterator中每一个值转换为promise
Promise.resolve(promise).then(
(value) => {
resolveCount++;
//这里不用push——需要按顺序返回
promiseResult[index] = value;
//如果遍历完所有iterator值
if (resolveCount===iterator.length) {
resolve(promiseResult);
}
},
//一旦有一个rejct就返回
(reason) => {
reject(reason);
}
);
});
});
//else为iterator不是可迭代对象
} else {
throw new TypeError(`${iterator} is not iterable`);
}
}
Promise.race()
Promise.race = function(itertor){
//是否是可迭代对象——itertor是对象或字符串,并且iterator[Symbol].iterator 类型是一个函数
const isIterable = ((typeof iterator === "object" && iterator !== null) || typeof iterator === "string") && typeof iterator[Symbol.iterator] === "function";
//是可迭代参数
if (isIterable) {
//将iterator转换为数组
iterator = Array.from(iterator);
return new Promise((resolve, reject) => {
iterator.forEach(promise => {
Promise.resolve(promise).then((res) => {
resolve(res)
})
.catch(e => {
reject(e)
})
});
});
}else{
throw new TypeError(`${iterator} is not iterable`);
}
}
Promise.any()
Promise.any = function(iterator){
//是否是可迭代对象——itertor是对象或字符串,并且iterator[Symbol].iterator 类型是一个函数
const isIterable = ((typeof iterator === "object" && iterator !== null) || typeof iterator === "string") && typeof iterator[Symbol.iterator] === "function";
//是可迭代参数
if (isIterable) {
let count = 0;
const promiseResult = [];
//将iterator转换为数组
iterator = Array.from(iterator);
return new Promise((resolve, reject) => {
//iterator为空返回[]
if (!iterator.length) {
reject(new AggregateError([],'All promises were rejected'))
}
iterator.forEach(promise => {
//将iterator中每一个值转换为promise
Promise.resolve(promise).then(
(value) => {
resolve(promise);
},(reason) => {
count++;
promiseResult.push(reason);
//如果遍历完所有iterator值
if (count===iterator.length) {
reject(new AggregateError(promiseResult,'All promises were rejected'));
}
}
);
});
});
//else为iterator不是可迭代对象
} else {
throw new TypeError(`${iterator} is not iterable`);
}
}`
Promise.allSelected()
Promise.allSettled = function (iterator) {
const isIterable = ((typeof iterator === "object" && iterator !== null) || typeof iterator === "string") && typeof iterator[Symbol.iterator] === "function";
//是可迭代参数
if (isIterable) {
return new Promise((resolve, reject) => {
if (!iterator.length) {
resolve([])
}
const results = [];
const len = iterator.length;
let resolvedCount = 0;
for (let i = 0; i < len; i++) {
Promise.resolve(iterator[i])
.then((value) => {
results[i] = { status: "fulfilled", value };
})
.catch((reason) => {
results[i] = { status: "rejected", reason };
})
.finally(() => {
resolvedCount++;
if (resolvedCount === len) {
resolve(results);
}
});
}
});
}else{
throw new TypeError(`${iterator} is not iterable`);
}
}
📕如何理解ES6中的Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
创建Proxy的方法
//方法1
var proxy = new Proxy(target,handler);
//举例handler——处理程序对象中定义的“基本操作的拦截器”
const handler = {
//get接收3个参数
get(target,prototype,receiver){
return target[prototype];
....
//target—代理对象 prototype——属性名 receiver——一般指向代理对象proxy
}
//set接收4个参数
set(target,prototype,value,receiver){
...
target[prototype] = value;
//value——对象prototype需要修改的属性值
}
}
//方法2——创建可撤销代理
const { proxy, revoke } = Proxy.revocable(data, handler)
target
表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理)
handler
通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p
的行为
注意:参数中的receiver
不能用于调用本身(如:console.log(receiver)
) 或 调用其属性(如:return receiver[prototype]
)相当于代理对象,调用其身上的属性会循环调用get()
,陷入死循环
handler解析
关于handler
拦截属性,有如下:
-
get(target,propKey,receiver):拦截对象属性的读取
该方法会拦截目标对象的以下操作:
- 访问属性:proxy[foo] 和 proxy.bar
- 访问原型链上的属性:Object.create(proxy)[foo]
- Reflect.get()
-
set(target,propKey,value,receiver):拦截对象属性的设置
-
has(target,propKey):拦截
propKey in proxy
的操作,返回一个布尔值 -
deleteProperty(target,propKey):拦截
delete proxy[propKey]
的操作,返回一个布尔值 -
ownKeys(target):拦截
Object.keys(proxy)
、for...in
等循环,返回一个数组 -
getOwnPropertyDescriptor(target, propKey):拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象 -
defineProperty(target, propKey, propDesc):拦截
Object.defineProperty(proxy, propKey, propDesc)
,返回一个布尔值 -
preventExtensions(target):拦截
Object.preventExtensions(proxy)
,返回一个布尔值 -
getPrototypeOf(target):拦截
Object.getPrototypeOf(proxy)
,返回一个对象 -
isExtensible(target):拦截
Object.isExtensible(proxy)
,返回一个布尔值 -
setPrototypeOf(target, proto):拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值 -
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作
-
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作
在handler直接定义操作拦截的问题(代理陷阱):
var obj2 = {
name:"mjn";
get value(){
return this.name
}
}
var obj1 = {name:"allen"}
const handler = {
//这里直接输出target[prototype] 是 obj2 中的prototype,而obj1调用向输出的是自己的prototype,因此会产生代理陷阱问题
get(target,prototype,receiver){
return targte[prototype];
//这里receiver指向obj1,而不是proxy
//解决方案
//return Refelct.get(...arguments)
}
}
const proxy = new Proxy(obj2,handler);
Object.setPrototypeOf(obj1,proxy);
console.log(obj1.value);// "mjn"而不是"allen"
Reflect API
若需要在Proxy
内部调用对象的默认行为,建议使用Reflect
,其是ES6
中操作对象而提供的新 API
const handeler = {
//使用方式1
get(){
return Reflect.get(...arguments);
}
//使用方式2
get:Reflect.get;
}
//使用方式3
const proxy = new Proxy(target,handler);
- 对于
Reflect.set()
、Reflect.defineProperty()
、Reflect.setPrototypeOf()
等这些设置方法都有状态标记,成功则返回true、否则返回false
Proxy的实际运用
-
跟踪属性访问
当属性进行set、get等方法时,可以在函数内部设置输出,监控对象属性状态
-
隐藏属性
const hiiden = ["foo","bar"]; const targte = { foo:1, bar:2, baz:3 } const proxy = new Proxy(target,{ get(target,prop){ if(hidden.includes(prop)){ return undefined; }else{ return Reflect.get(...arguments); } } })
-
属性验证
例如:在修改属性的时候可以在set中设置if(满足条件)再进行 Reflect.set(…arguments)
-
函数与构造函数参数验证
proxy在handler中提供
apply()
和constructor()
用来捕获函数调用 和 构造函数创建实例const proxy = new Proxy(target,{ apply(target,thisArg,argumentsList){ for(const arg of argumentsList){ if(typeof arg != 'number'){ throw "不是number类型参数" } } return Relfect.apply(...arguments); } }) const User{ constructor(id){ this.id = id; } } const proxy = new Proxy(User,{ constructor(target,argumentList,newTarget){ if(argumentList[0]===undefined){ throw "没有id无法创建User实例" }else{ return Reflect.constructor(...arguments); } } })
📕如何理解ES6中的Class
类的定义
class可以理解为是一个语法糖,js中没有正式的类这个类型,它本质是一种特殊的函数
class User{}
console.log(typeof Person) //function
定义类有两种方式:类声明 和 类表达式
//类声明
class Person{}
//类声明式
const Animal = class {};
-
函数声明可以提升,但是类声明不能
-
函数受函数作用域限制,而类受块作用域限制
{ function Fn(){} class User{} } console.log(Fn) // Fn(){} console.log(User) //未定义
类的构造函数
当我们通过new
实例化一个对象的过程:
(1)内存中创建一个新对象
(2)这个新对象内部的[[prototype]]指针被赋值为构造函数的prototype属性
(3)构造函数内部的this被赋值为这个新对象(this指向新对象)
(4)执行构造函数内部的代码(给新对象添加属性)
(5)如果构造函数返回非空对象,则返回该对象;否则返回刚创建的对象
//手动实现new
function nyNew(fn,...args){
//内存中创建一个新对象
let obj = {};
//这个新对象内部的[[prototype]]指针被赋值为构造函数的prototype属性
obj.__proto__ = fn.prototype;
//this指向新对象 + 执行构造函数内部的代码
//这里result主要解决构造器返回对象数据的情况
let result = fn.apply(obj,...args);
//如果构造函数返回非空对象,则返回该对象;否则返回刚创建的对象
return result instanceof Object ? result : obj;
}
- 如果返回的不是创建的实例对象,而是构造器中自己return的对象,那么这个对象不会通过instanceof操作符检测出和类有关联
实例、原型 和 类成员的属性方法
class Person{
constructor(){
//添加到所有实例上
this.locate = ()=>console.log("instance",this);
}
//添加在类的原型上
locate(){
console.log('prototype',this)
}
//添加在类本身上
static locate(){
console.log('class',this)
}
}
-
类块中直接定义的方法都会添加为原型方法,可以在实例间共享
-
类块中不能直接给原型添加原始值或对象作为成员数据(但会添加到实例对象上),但可以添加方法
-
类方法等同于对象属性,可以使用字符串、符号、计算值作为键
class Person{ sayNumber(){ console.log(123) } [sayBaby](){ console.log("baby") } ['say'+'nihao'](){ console.log("nihao") } }
-
可以在类中定义静态方法(使用stataic关键字作为前缀),定义的静态方法属于类本身。静态方法里面的this指向的是类而不是实例
-
虽然不能再类块内添加原始值、对象这些成员数据,但是可以再类块外手动添加
class Person{ sayName(){ console.log(Person.name) } } Person.name = "mjn"; Person.prototype.name = "niho"
-
类支持在原型 和 类本身上定义生成器方法
class Person{ *create{ yield 'mjn'; yield "jake"; yield "jack" } static *create{ yield 'mmm'; yield "nnn"; yield "eee" } }
类的继承
类的继承通过extends实现,可以继承任何具有[[constructor]] 和原型的对象(也就是说也可以通过extends
继承构造函数),背后使用的依旧是原型链
类可以通过super关键字引用它们的原型,使用super
应该注意:
-
super只能在派生类(子类)构造函数(=>用于调用父类构造函数) 和 静态方法中使用(=>用于调用父类的静态方法)
-
super在派生类的构造函数中使用的时候,可以传参(用于给父类的构造函数传参)
-
派生类显示的定义了构造函数,则必须要在其中调用super(),否则就返回一个对象。并且不能在super()之前引用this
-
类在继承时属性会被直接添加到实例中,方法则保留在类的原型上
//S.prototype.__proto__ === F.prototype class F { money = '100w' //实例属性 fn () {} //原型上方法 } class S extends F{ }
📕如何理解ES6中的迭代器(iterator)
可迭代对象
早期执行迭代必须使用循环等辅助结构,代码会十分混乱,因此ES6以后开始支持了迭代器模式。一些数据结构具有键为[Symbol.interator]
的属性(如:字符串、数组、集合、映射(Map)、arguments等),称为可迭代对象。
注意 : Object不是可迭代对象,因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。
[Symbol.interator]
当启动for...of
循环时,会调用[Symbol.interator]
方法,这个方法必须返回一个迭代器,迭代器内具有 next()
方法,每一轮循环调用一次next()
方法,取得下一个成员数据。每一次调用next方法返回一个包含value
和done
两个属性的对象。其中,value
属性是当前成员的值,done
属性是一个布尔值,表示遍历是否结束(true则结束)
-
不同迭代器的实例相互没有联系,会独立遍历对象
let arr = ['foo','bar'] let iter1 = arr[Symbol.iterator] let iter2 = arr[Symbol.iterator] //iter1 iter2迭代互不干涉
-
如果可迭代对象在迭代期间被修改了,迭代器也会反应相应变化
-
一旦done为true,后面调next()就一直返回相同的值
实现一个可迭代对象:
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
//返回一个迭代器
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
} else {
return { value: undefined, done: true };
}
}
};
}
};
for (var i of obj){
console.log(i);
}
接收可迭代对象的操作:
- for…of
- 解构赋值、扩展运算符
- Array.from
- 创建集合
- 创建映射(new Map(这里传入可迭代对象))
- Promise.all
- Promise.race
- yield*操作符…
HTTP部分
📕常见的状态码有哪些
100
:客户端在发送POST数据给服务器前,先发送请求头,征询服务器情况,看服务器是否处理POST的数据,如果不处理,客户端则不上传POST数据,如果处理,则POST上传数据。常用于POST大数据传输200
表示从客户端发来的请求在服务器端被正常处理了。204
表示请求处理成功,但没有资源返回。301
表示永久性重定向。该状态码表示请求的资源已被分配了新的URI,以后应使用资源现在所指的URI。302
表示临时性重定向。304
:协商缓存,告诉客户端有缓存,直接使用缓存中的数据,返回页面的只有头部信息,是没有内容部分400
表示请求报文中存在语法错误。当错误发生时,需修改请求的内容后再次发送请求。401
表示未授权(Unauthorized),当前请求需要用户验证403
表示对请求资源的访问被服务器拒绝了404
表示服务器上无法找到请求的资源。除此之外,也可以在服务器端拒绝请求且不想说明理由时使用。500
表示服务器端在执行请求时发生了错误。也有可能是Web应用存在的bug或某些临时的故障。503
表示服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。
📕GET 和 POST请求的区别
GET通常用于请求获取指定资源,而POST通常用于将实体提交到指定的资源。本质都是TCP连接,但是由于HTTP
的规定和浏览器/服务器的限制,导致他们在应用过程中会体现出一些区别。
区别
- GET在浏览器回退时是无害的,而POST会再次提交请求。
- GET产生的URL地址可以被收藏,而POST不可以。
- GET请求会被浏览器主动缓存,而POST不会,除非手动设置。
- GET请求只能进行url编码,而POST支持多种编码方式。
- GET参数通过URL传递,POST放在Request body中
- 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。
- GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
- GET请求在URL中传送的参数是有长度限制的,而POST没有。
- GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
参数长度:HTTP
协议没有Body
和 URL
的长度限制,对 URL
限制的大多是浏览器和服务器考虑处理较长的url
会消耗较多资源,出于性能和安全的考虑限制了url长度
安全:本质上两者都是不安全的。因为HTTP
在网络上是明文传输的,通过技术手段能完整地获取数据报文,只有使用HTTPS
才能加密安全
数据包 :于GET
方式的请求,浏览器会把http header
和data
一并发送出去,对于POST
,浏览器先发送header
,服务器响应100 continue
,浏览器再发送data
。但并不是所有浏览器都会在POST
中发送两次包
📕浏览器的同源策略
什么是源
如果两个URL的协议、主机(域名)和端口号都相同的话,则这两个URL就是同源。同源策略是一个重要的安全策略,它定义了源之间资源如何交互,减少可能被攻击的媒介。
跨域网络访问
同源策略可以控制不同源之间的交互,例如在发送Ajax
、fetch
请求 或使用 <img>
标签时则会受到同源策略的约束。这些交互通常分为三类:
- 跨源写操作(Cross-origin writes)一般是被允许的。例如链接、重定向以及表单提交。
- 跨源资源嵌入(Cross-origin embedding)一般是被允许的(后面会举例说明)。
- 跨源读操作(Cross-origin reads)一般是不被允许的,但常可以通过内嵌资源来巧妙的进行读取访问
可能嵌入跨源的资源的一些示例:
- 使用
<script src="…"></script>
标签嵌入的 JavaScript 脚本。语法错误信息只能被同源脚本中捕捉到。 - 使用
<link rel="stylesheet" href="…">
标签嵌入的 CSS。由于 CSS 的松散的语法规则,CSS 的跨源需要一个设置正确的Content-Type
标头。 - 通过
<img>
展示的图片。 - 通过
<video>
和<audio>
播放的多媒体资源。 - 通过
<object>
和<embed>
嵌入的插件。 - 通过
@font-face
引入的字体。一些浏览器允许跨源字体(cross-origin fonts),另一些需要同源字体(same-origin fonts)。 - 通过
<iframe>
载入的任何资源。
如何解决跨域
jsonp解决同源限制问题
通过<script>
标签的src属性实现跨域。<script>
属于跨源资源嵌入,因此不受同源策略限制。
-
实现jsonp封装
function jsonp(options){ var script = document.createElement("script"); script.src = options.url; document.body.appendChile(srcipt); script.onload = function(){ document.removeChile(srcipt); } }
-
弊端:
-
jsonp只能发送GET请求
-
jsonp没有办法返回后端状态码,没办法改变
-
存在安全隐患,服务端直接调用方法(不能保证服务器的可靠性)
-
CORS实现跨域请求
是一种基于 HTTP
头的机制,该机制通过允许服务器标示除了它自己以外的其它源(域、协议或端口不同),使得浏览器允许这些源访问加载自己的资源。
浏览器发出的跨域请求分为两种:
-
简单请求
满足所有以下条件:
- 请求方法:
GET,HEAD,POST
- 请求头的内容—HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
当浏览器发现发现的ajax请求是简单请求时,会在请求头中携带一个字段:
Origin``Origin
中会指出当前请求url,服务会根据这个值决定是否允许其跨域。如果服务器允许跨域,需要在返回的响应头中携带
Access-Control-Allow-Origin
:上面Origin
中的地址或者*代表任意。 - 请求方法:
-
特殊请求
不符合简单请求的条件,会被浏览器判定为特殊请求。
特殊请求会在正式通信之前,浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的
XMLHttpRequest
请求,否则就报错。与简单请求相比,除了Origin以外,多了两个头:
- Access-Control-Request-Method:接下来会用到的请求方式,比如PUT
- Access-Control-Request-Headers:会额外用到的头信息
服务的收到预检请求,如果许可跨域,会发出响应,除了简单请求中的两个,还附加:
- Access-Control-Allow-Methods:允许访问的方式
- Access-Control-Allow-Headers:允许携带的头
- Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了。
📕DNS协议 是什么?说说DNS 完整的查询过程?
DNS(Domain Names System)解释为域名系统,属于是应用层的协议,用于将用户提供的主机名(域名)解析为 IP 地址。
域名是一个具有层次的结构,从上到下依次为根域名、顶级域名、二级域名、三级域名等…
例如www.baidu.com
,www
为三级域名、baidu
为二级域名、com
为顶级域名,系统为用户做了兼容,域名末尾的根域名.
一般不需要输入,域名的每层都设有一个域名服务器:
-
根域名服务器(Root DNS Server):管理顶级域名服务器,返回“com”“net”“cn”等顶级域名服务器的 IP 地址
-
顶级域名服务器(Top-level DNS Server):管理各自域名下的权威域名服务器,比如 com 顶级域名服务器可以返回 baidu.com 域名服务器的 IP 地址
-
权威域名服务器(Authoritative DNS Server):管理自己域名下主机的 IP 地址,比如 baidu.com 权威域名服务器可以返回 www.baidu.com的 IP 地址
-
本地域名服务器(Local Name Server,local DNS):如果通过 DHCP 配置,由互联网服务提供商(ISP,如联通、电信)提供
![](https://img-blog.csdnimg.cn/img_convert/4ed80481c5010db1f390989974f16c38.png)
DNS查询方式
DNS 查询有两种方式:递归 和 迭代 。
客户端与本地域名服务器之间一般采用递归查询,它负责全权处理客户端的 DNS 查询请求,直到返回最终结果
而 DNS 根域名服务器之间一般采用 迭代查询 方式,当DNS 服务器查不到该域名,它不会替客户端完成后续的查询工作,而是回复下一步应当向哪一个域名服务器进行查询
- 递归查询
![](https://img-blog.csdnimg.cn/img_convert/637b37905aa8398d3edf09e9ffba513f.webp?x-oss-process=image/format,png)
- 迭代查询
![](https://img-blog.csdnimg.cn/img_convert/bf4304664d67b87a234faa5d879f2515.webp?x-oss-process=image/format,png)
域名缓存
一次完整的 DNS 查询过程需要访问多台 DNS 服务器才能得到最终的结果,这会带来一定的时延。为了改善时延,DNS 服务并不是每次请求都要去访问 DNS 服务器,而是访问过一次后将 DNS 记录缓存在本地。
计算机中 DNS 记录在本地有两种缓存方式:浏览器缓存和操作系统缓存
浏览器缓存:浏览器在获取网站域名的实际 IP 地址后会对其进行缓存,减少网络请求的损耗。但是缓存并不是永久有效的,如 Chrome 的过期时间是 1 分钟,在这个期限内不会重新请求 DNS
操作系统缓存:操作系统的缓存其实是用户自己配置的 hosts 文件。
访问顺序为:浏览器缓存 -> 操作系统缓存 -> 路由器缓存 -> local DNS 缓存 -> DNS 查询
查询过程
-
首先搜索浏览器的 DNS 缓存,缓存中维护一张域名与 IP 地址的对应表
-
若没有命中,则继续搜索操作系统的 DNS 缓存
-
若仍然没有命中,则操作系统将域名发送至本地域名服务器,本地域名服务器采用递归查询自己的 DNS 缓存,查找成功则返回结果
-
若本地域名服务器的 DNS 缓存没有命中,则本地域名服务器向上级域名服务器进行迭代查询
- 首先本地域名服务器向根域名服务器发起请求,根域名服务器返回顶级域名服务器的地址给本地服务器
- 本地域名服务器拿到这个顶级域名服务器的地址后,就向其发起请求,获取权限域名服务器的地址
- 本地域名服务器根据权限域名服务器的地址向其发起请求,最终得到该域名对应的 IP 地址
-
本地域名服务器将得到的 IP 地址返回给操作系统,同时自己将 IP 地址缓存起来
-
操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起
-
至此,浏览器就得到了域名对应的 IP 地址,并将 IP 地址缓存起
📕TCP的三次握手与四次挥手
TCP 提供面向有连接的通信传输,面向有连接是指在传送数据之前必须先建立连接,数据传送完成后要释放连接。连接是通过三次握手进行初始化的,并且由于TCP是全双工模式,所以需要四次挥手关闭连接
TCP序号和确认号
32位序号 seq:TCP通信过程中某一个传输方向上的字节流的每个字节的序号,通过这个来确认发送的数据有序,比如现在序列号为1000,发送了1000,下一个序列号就是2000。
32位确认号 ack:TCP对上一次seq序号做出的确认号,给收到的TCP报文段的序号seq加1,用来响应TCP报文段
TCP标志位
用的最广泛的标志是 SYN,ACK 和 FIN,用于建立连接,确认成功的段传输,最后终止连接。
-
SYN:简写为S,同步标志位,用于建立会话连接,同步序列号;
-
ACK: 简写为.,确认标志位,对已接收的数据包进行确认;
-
FIN: 简写为F,完成标志位,表示我已经没有数据要发送了,即将关闭连接;
-
PSH:简写为P,推送标志位,表示该数据包被对方接收后应立即交`给上层应用,而不在缓冲区排队;
-
RST:简写为R,重置标志位,用于连接复位、拒绝错误和非法的数据包;
-
URG:简写为U,紧急标志位,表示数据包的紧急指针域有效,用来保证连接不被阻断,并督促中间设备尽快处理;
TCP三次握手
进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备,TCP三次握手的过程为:
第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号。此时客户端处于 SYN_SEND 状态。
首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。
第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。
在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y。
第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。
确认报文段ACK=1,确认号ack=y+1,序号seq=x+1(初始为seq=x,第二个报文段所以要+1)
为什么是三次握手:第一次握手服务端确定自己的接收能力正常、客户端的发送能力正常;第二次握手客户端确定自己的接收发送能力正常,服务端接受能力和发送能力正常;至此,虽然客户端已经知道能够正常传输接收数据,但是服务器还并不能确认自己发送的数据对方是否能够接收,因此还需要客户端发送确认报文
TCP四次挥手
TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力,称之为半关闭。这使得终止TCP连接需要经历四次挥手,客户端或服务器均可主动发起挥手动作。假如是客户端先发起关闭请求,四次挥手的过程如下:
第一次挥手 客户端发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u,此时,客户端进入FIN-WAIT-1(终止等待1)状态
第二次挥手 服务器端接收到连接释放报文后,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT 关闭等待状态
第三次挥手 客户端接收到服务器端的确认请求后,客户端就会进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文,服务器将最后的数据发送完毕后,就向客户端发送连接释放报文(FIN=1,ACK=1,序号seq=w,确认号ack=u+1),服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
第四次挥手 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态,但此时TCP连接还未终止,为了确保服务端收到自己的ACK报文,客户端必须要经过2MSL后(最长报文寿命),才会进入CLOSED关闭状态,服务器端接收到确认报文后,会立即进入CLOSED关闭状态,到这里TCP连接就断开了,四次挥手完成
📕回流和重绘
回流与重绘
什么是回流:当我们对 DOM 的修改引发了 DOM 布局或者几何属性时(比如修改元素的宽、高或隐藏元素等),浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来。这个过程就是回流(也叫重排)。
- 全局范围:从根节点 html 开始对整个渲染树进行重新布局。
- 局部范围:对渲染树的某部分或某一个渲染对象进行重新布局
什么是重绘:当我们对 DOM 的修改导致了样式的变化、却并未影响其几何属性(比如修改了颜色或背景色)时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式。这个过程叫做重绘。
回流必将引起重绘,重绘不一定会引起回流
常见引起回流的设计
场景:
- 页面一开始渲染的时候(这肯定避免不了)
- 添加或者删除可见的
DOM
元素; - 元素的位置发生变化
- 元素尺寸改变——边距、填充、边框、宽度和高度;
- 浏览器窗口尺寸改变——resize 事件发生时
- 计算 offsetWidth 和 offsetHeight 属性
属性和方法
- width、height、margin、padding、border
- display、position、overflow
- clientWidth、clientHeight、clientTop、clientLeft
- offsetWidth、offsetHeight、offsetTop、offsetLeft
- scrollWidth、scrollHeight、scrollTop、scrollLeft
- scrollIntoView()、scrollTo()
- getComputedStyle()
- getBoundingClientRect()
- scrollIntoViewIfNeeded()
常引起重绘的属性和方法
- color
- border-style
- visibility
- background
- text-decoration
- background-image
- background-position
- background-repeat
- outline-color
- outline
- outline-style
- border-radius
- outline-width
- box-shadow
- background-size
浏览器优化机制
由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列
但是你获取布局信息的操作的时候,会强制队列刷新,包括前面讲到的如offsetTop等方法都会返回最新的数据,因此浏览器不得不清空队列,触发回流重绘来返回正确的值
减少回流重绘方案
- 读写分离操作
div.style.left = "10px";
div.style.top = "10px";
div.style.width = "20px";
div.style.height = "20px";
// 分离引用读取
console.log(div.offsetLeft);
console.log(div.offsetTop);
console.log(div.offsetWidth);
console.log(div.offsetHeight);
- 样式集中操作
div.style.left = "10px";
div.style.top = "10px";
div.style.width = "20px";
div.style.height = "20px";
虽然现在大部分浏览器有渲染队列优化,不排除有些浏览器以及老版本的浏览器效率仍然低下:建议通过改变 class 或者 csstext 属性集中改变样式
// bad
var left = 10;
var top = 10;
el.style.left = left + "px";
el.style.top = top + "px";
// good
el.className += " theclassname";
// good
el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
el.style.cssText += `; left:${left}px; top:${top}px;`;
- 缓存布局信息
// bad
div.style.left = div.offsetLeft + 1 + "px";
div.style.top = div.offsetTop + 1 + "px";
// good 缓存布局信息 相当于读写分离 ;想深入了解缓存优化参考 《小鹦鹉》
var curLeft = div.offsetLeft;
var curTop = div.offsetTop;
div.style.left = curLeft + 1 + "px";
div.style.top = curTop + 1 + "px";
curLeft = curTop = null;
- "离线"改变 DOM
(1)隐藏要操作的 dom,在要操作 dom 之前,通过 display 隐藏 dom,当操作完成之后,才将元素的 display 属性为可见,因为不可见的元素不会触发重排和重绘。
(2)通过使用文档碎片创建一个 dom 碎片,在它上面批量操作 dom,操作完成之后,再添加到文档中,这样只会触发一次重排。document.createDocumentFragment()
- 优化动画
动画效果还应牺牲一些平滑,来换取速度,这中间的度自己衡量:比如实现一个动画,以 1 个像素为单位移动这样最平滑,但是回流就会过于频繁,大量消耗 CPU 资源;举例优化如果以 3 个像素为单位移动,则会好很多。
- 启用 GPU 加速
GPU 硬件加速是指应用 GPU 的图形性能对浏览器中的一些图形操作交给 GPU 来完成,它在速度和能耗上更有效率。
GPU 加速通常包括以下几个部分:Canvas2D,布局合成, CSS3 转换(transitions),CSS3 3D 变换(transforms),WebGL 和视频(video)。
📕浏览器页面渲染
页面渲染过程
-
解析HTML,生成DOM树,解析CSS,生成CSSOM树
-
将DOM树和CSSOM树结合,去除不可见元素(很重要),生成渲染树(Render Tree)
-
Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
-
Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
-
Display:将像素发送给GPU,展示在页面上
什么是不可见节点
- 一些不会渲染输出的节点,比如 script、meta、link 等。
- 一些通过 css 进行隐藏的节点。注意,使用 visibility 和 opacity 隐藏的节点,还是会显示在渲染树上的(因为还占据文档空间),只有 display : none 的节点才不会显示在渲染树上。
渲染树
阻塞渲染
css阻塞
css阻塞DOM解析:当JavaScript 中访问了某个元素的样式,那么这时候就需要 等待这个样式被下载 完成才能继续往下执行,从而css间接阻塞了DOM解析
css阻塞渲染树:CSSOM树的构建阻塞渲染树的构建
js阻塞
当 HTML 解析器遇到一个 script 标记时,它会暂停构建 DOM,将控制权移交给 JS 引擎,直到 JS 引擎运行完毕
如何解决css阻塞
CSS引入的位置 —— 针对阻塞 DOM 解析
一般我们把<style>
、<link>
放在<head>
里面,提前加载好CSS资源,这样当 JavaScript 请求到样式表时将不必等待 CSS 资源的加载
媒体查询的方式 —— 针对阻塞 render 树的构建
有些 CSS 资源在首次渲染中可能不用用到,只是在用户交互(比如改变页面大小)时才会用到,所以我们通过媒体查询的方式来判断是否需要在首次渲染加载。这样从某种程度上会减少首屏加载时间
<!-- 适用于所有情况,始终阻塞渲染 -->
<link href="style.css" rel="stylesheet">
<!-- 网页首次加载时,只在打印内容时适用 -->
<link href="print.css" rel="stylesheet" media="print">
<!-- 如果不是在打印内容时,该样式表不阻塞渲染 -->
<!-- 符合条件时浏览器将阻塞渲染,直至样式表下载并处理完毕 -->
<link href="other.css" rel="stylesheet" media="(max-width: 400px)">
<!-- 如果不满足条件,不会阻塞渲染,但依旧会请求下载对应的资源 -->
如何解决js阻塞
<script>
标签引入位置
如果页面渲染内容为<script>
标签请求的内容,则该<script>
标签一般需要放在<head>
里面
如果页面渲染内容跟<script>
标签内容无关的话,比如说 DOM 事件、加载其他(还未见的)内容,则该<script>
标签一般放在<body>
标签里的最后位置
- defer 和 async 属性
如果需要某段 JavaScript 代码需要提前加载,即可能会放在<head>
里面或某些 DOM 节点前面,则给<script>
标签添加 defer 或 async 属性:
- 如果加载完需要 立刻执行 则使用
async
属性; - 如果加载完不需要立刻执行,想要在页面结构加载完(
window.onload
)再立刻执行的话,使用defer
属性; 值得注意的是:
没有使用这两种属性之一的话,则 JavaScript 的加载和运行都会阻塞渲染,有的话只有运行会阻塞
📕说说地址栏输入 URL 敲下回车后发生了什么?从输入URL的行
在浏览中输入 URL 并且获取响应的过程,其实就是浏览器和该 URL 对应的服务器的网络通信过程。从输入 URL
到回车后发生的行为主要有几个过程:
URL解析
—> DNS 查询
—>TCP 连接
—>HTTP 请求
—>响应请求
—>页面渲染
URL解析
当用户输入一个完整的URL
之后,浏览器就开始解析URL
的构成。一个URL
的结构是由这些部分组成的:
DNS查询
在用户输入的URL
中包含域名,然而域名并不是目标服务器真正意义上的地址,我们需要通过DNS解析获取相对应的IP地址,查询过程如下(详见DNS协议查询过程,这里不详述):
![](https://img-blog.csdnimg.cn/img_convert/73c39c34c08063b960ce4ff781b6ca28.png)
TCP连接
tcp
是一种面向有连接的传输层协议,在确定目标服务器服务器的IP
地址后,则经历三次握手建立TCP
连接(之前有讲过,这里不细讲),保证双方都具有可靠的接收和发送能力(详见TCP三次握手)
浏览器发送请求
当建立tcp
连接之后,就可以在这基础上进行通信,浏览器发送 http
请求到目标服务器
请求的内容包括:
- 请求行
- 请求头
- 请求主体
![](https://img-blog.csdnimg.cn/img_convert/2ef7f597ea3c3d37afdb30c23de14791.png)
服务器响应请求
当服务器接收到浏览器的请求之后,就会进行逻辑操作,处理完成之后返回一个HTTP
响应消息,包括:
- 状态行
- 响应头
- 响应正文
断开TCP连接
浏览器和服务器都不再需要发送数据后,四次挥手断开 TCP 连接,任意一方都可以发起关闭请求(详见TCP四次挥手)
页面渲染
关于页面的渲染过程如下(详见浏览器页面渲染):
- 解析HTML,构建 DOM 树
- 解析 CSS ,生成 CSS 规则树
- 合并 DOM 树和 CSS 规则,生成 render 树
- 布局 render 树( Layout / reflow ),负责各元素尺寸、位置的计算
- 绘制 render 树( paint ),绘制页面像素信息
- 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成( composite ),显示在屏幕上
📕web性能优化
web性能是客观的衡量标准,是用户对加载时间和运行的直观体验。web性能指页面加载到可交互和可响应所消耗的时间,以及页面在交互时的流畅度。
web性能报告
我们可以通过Chrome的扩展程序lighthouse、Pingdom等工具得到FCP(First Contentful Paint)白屏时间、SI(Speed Index)页面渲染时间、TBT(Total Blocking Time)用户行为阻塞时间等,从而分析网页每部分的性能问题,同时它们也会给出相应的优化意见可以供我们参考,如果我们想要优化性能可以从以下几个方面入手:
提高资源响应速度
资源响应速度的主要优化点在于:减少请求数、减少请求资源体积、提升网络传输效率
-
使用 CDN 加速:内容分发网络,通过设置边缘结点缩短用户与服务端地理距离,从而来缩短请求静态资源时间。(详见如何理解CDN)
-
开启gzip压缩:使用 gzip 压缩编码技术,减小资源体积。gzip通常需要web 服务器和客户端(浏览器)必须同时支持 gzip。gzip 压缩效率非常高,通常可达 70% 压缩率
-
浏览器缓存:前端缓存一般可分为 http缓存和浏览器缓存,http缓存还分强缓存和协商缓存,浏览器缓存,比如
localStorage
,sessionStorage
,cookie
等(详见浏览器缓存策略) -
减少网络请求次数和体积:通过压缩文件及合并小文件为大文件,减少网络请求次数,但需要找到合理的平衡点。
-
使用 HTTP/2(HTTP2的优势见HTTP1.0/HTTP1.1/HTTP2.0的区别)
资源体积优化
-
文本资源(包括
HTML
、css
、js
等):组件按需加载、代码按需打包、按需引入第三方样式文件等 -
图片资源
字体图标代替图片图标
一些通用的小图标,如箭头,叉,可以使用字体图标,减少请求,渲染更快精灵图(没有字体图标效果好)
一些带有企业特色的小图标,如淘宝购物车,笑脸娃娃,可以使用精灵图,让一张图上带有多个小图,然后使用css背景定位来显示出合适的位子,能大大减少请求图片懒加载
为了首屏渲染更快,图片可设置一张加载图代替,当页面在可视区域内时在替换为真正的图片
如果有首屏很大的高清图,可先渲染清晰度低的缩略图,在首页基本构建完成下一次事件循环再去替换为高清图图片预加载
可以在window.onload之后请求一些其他地方需要的图片资源
比如我们有一个活动页使用了高清图,我们可以在它的入口前的首页就加载它,当我们进去页面时,浏览器就会从缓存里读取这张图片使用png格式的图片
PNG 格式是WEB 图像中最通用的格式,它是一种无损压缩格式小于10k的图片可以打包为base64格式
可以使用webpack url-loader处理优化代码质量
提高代码质量
- 精简代码:
- 使用
lodash
提供的功能函数 - 使用ES6语法
- 去除无效代码、封装共用模块代码提高代码耦合性
- 使用
- 代码性能:
- 代码设计:
- 优化长列表—使用
virtual-list
方案只给DOM添加当前屏幕显示的DOM节点 - 避免 ifarme 嵌套网页
- 优化长列表—使用
- 降低内存占用
- 合理使用闭包,防止内存泄漏
- 将使用的定时器及时清除
- 避免循环引用
- DOM删除时解绑事件等
完善用户体验
在设计页面的时候考虑到用户的习惯性行为,在用户操作的过程中给出网页使用的引导,在页面加载遇到问题时给与友好提示、考虑到用户的视觉变化在设计弹窗等网页行为时给一个过渡的效果等等…
📕如何理解CDN 说说实现原理
CDN,即内容分发网络。它是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。
CDN原理分析
在没有应用CDN
时,我们使用域名访问某一个站点时的路径为
用户提交域名→浏览器对域名进行解释→
DNS
解析得到目的主机的IP地址→根据IP地址访问发出请求→得到请求数据并回复
应用CDN
后,DNS
返回的不再是 IP
地址,而是先返回一个CNAME
(Canonical Name ) 别名记录,指向CDN
的全局负载均衡,这个全局负载均衡系统会综合用户地理位置、运营商网络、边缘结点的负载情况等因素进行智能调度,计算最合适的边缘节点并返回给用户,具体流程如下:
①、当用户点击APP上的内容,APP会根据URL地址去本地DNS(域名解析系统)寻求IP地址解析。
②、本地DNS系统会将域名的解析权交给CDN专用DNS服务器。
③、CDN专用DNS服务器,将CDN的全局负载均衡设备IP地址返回用户。
④、用户向CDN的负载均衡设备发起内容URL访问请求。
⑤、CDN负载均衡设备根据用户IP地址,以及用户请求的内容URL,选择一台用户所属区域的缓存服务器。
⑥、负载均衡设备告诉用户这台缓存服务器的IP地址,让用户向所选择的缓存服务器发起请求。
⑦、用户向缓存服务器发起请求,缓存服务器响应用户请求,将用户所需内容传送到用户终端。
⑧、如果这台缓存服务器上并没有用户想要的内容,那么这台缓存服务器就要网站的源服务器请求内容。
⑨、源服务器返回内容给缓存服务器,缓存服务器发给用户,并根据用户自定义的缓存策略,判断要不要把内容缓存到缓存服务器上。
CDN缓存代理
缓存系统是 CDN
的另一个关键组成部分,缓存系统会有选择地缓存那些最常用的那些资源,其中有两个衡量CDN
服务质量的指标:
- 命中率:用户访问的资源恰好在缓存系统里,可以直接返回给用户,命中次数与所有访问次数之比
- 回源率:缓存里没有,必须用代理的方式回源站取,回源次数与所有访问次数之比
缓存系统也可以划分出层次,分成一级缓存节点和二级缓存节点。一级缓存配置高一些,直连源站,二级缓存配置低一些,直连用户
回源的时候二级缓存只找一级缓存,一级缓存没有才回源站,可以有效地减少真正的回源
现在的商业 CDN
命中率都在 90% 以上,相当于把源站的服务能力放大了 10 倍以上
📕如何理解浏览器缓存策略(HTTP缓存)
HTTP缓存:缓存命中机制主要分为两个阶段—强缓存和**协商缓存。**浏览器是否启动缓存,主要是服务器来设置。当该资源首次被请求时,服务器通过设置HTTP响应的响应头来设置该资源的缓存信息
相同点:其中无论是哪种缓存命中,最终使用的都是浏览器缓存到本地的资源
不同点:强缓存不发生网络请求。
强缓存
定义:强缓存主要是浏览器自行判断资源是否过期,如果不过期则直接使用缓存的资源(强缓存命中),不再进行网络请求
实现:强缓存是利用Expires
、Cache-Control
或者Pragma
这三个http response header实现的
优先级Pragma
> Cache-Control
> Expires
,其中Pragma
不常用
Expires
- HTTP 1.0 用于缓存管理的
header
字段,由服务器返回值表示一个资源过期的时间,描述的是属于服务端时间系统的一个绝对时间 - 判断方法:浏览器发起下一次请求时,当前HTTP发起的请求时间
(this http request time) < (expires设置的值)
,资源没有过期,缓存命中。 - 弊端:
Expires
遵循的是服务端的时间系统,而请求时间遵循的是客户端的时间系统,如果两者时间不是一致的,就可能产生误差
Cache-Control
为了解决Expires因为客户端和服务器端时间不统一带来的问题,HTTP 1.1 提出了 Cache-Control
,这个字段使用相对时间以秒为单位、用数值表示,进行比较的时候用的都是客户端的时间,相对来更有效与安全。
- 判断方法:浏览器发起下一次请求时,当前HTTP发起的请求时间
(this http request time) < (last http request time + cache-control 设置的值)
,资源没有过期,缓存命中。
该header
字段的其他取值如下:
字段名 | 位置 | 说明 |
---|---|---|
no-cache | 请求头,响应头 | 强制客户端向服务器发送请求(禁止强缓存) |
no-store | 请求头,响应头 | 禁止一切缓存。客户端和代理服务器都不能缓存响应。 |
max-age | 请求头,响应头 | 设置资源可以被缓存多长时间,单位是秒。 |
no-transform | 请求头,响应头 | 代理不可更改媒体类型 |
cache-extension | 请求头,响应头 | 新指令标记(token) |
s-maxage | 响应头 | 和max-age同理,只不过是针对代理服务器缓存而言。 |
private | 响应头 | 不能被代理服务器缓存 |
public | 响应头 | 响应可以被任何缓存区缓存 |
must-revalidate | 响应头 | 在缓存过期前可以使用,缓存过期以后必须向服务器验证。 |
proxy-revalidate | 响应头 | 要求中间缓存服务器对缓存的响应有效性需再次确认(代理服务器需要发送请求给服务器端确认资源有效性,不能直接返回缓存) |
only-if-cached | 请求头 | 从缓存中获取资源 |
min-fresh | 请求头 | 单位:秒,期望在指定的时间内,响应仍有效 |
max-stale | 请求头 | 单位:秒, 接受已过期的响应 |
Pragma优先级最高,就一个值no-cache
等同于Cache-Control
中 no-cache
缓存命中:当强缓存命中时,HTTP状态码为200,资源从缓存中加载( from memory cache
/ from disk cache
)
from memory cache
:是把资源存到内存中,当进程退出时(关闭浏览器),内存中的数据会清空from disk cache
:是把资源缓存在磁盘中,进程退出时不受影响
协商缓存
在强缓存阶段无法命中的情况下,浏览器发起请求,询问服务器是否可以使用本地缓存资源,如果服务器检查发现浏览器本地的资源没有过期,则返回304告诉浏览器可以使用本地的缓存资源(协商缓存命中),否则返回200正常响应,协商缓存主要由``Last-Modified 和
Etag 两个
HTTP头`实现:
Last-Modified / If-Modified-Since
浏览器发送请求时,会将上次响应头中的 Last-Modified
赋值给 本次请求头中的 If-Modified-Since
字段。服务端中接收到请求之后,会将这个字段和当前资源最后的修改时间做对比,
- 如果
If-Modified-Since
(上一次资源修改时间) < 服务器上资源的最后修改时间(发送请求时间晚于服务器资源最后修改时间),则说明当前资源被修改过了,服务端需要返回新的资源,此时响应200,返回正常的响应。同时这次响应会返回新的Last-Modified
值,用于更新浏览器缓存。 - 如果
If-Modified-Since
(上一次资源修改时间)≥ 服务器上资源的最后修改时间,则说明没有修改过资源,则返回304状态码,不会返回资源内容。
弊端:
- 短时间内资源发生了变化,这个字段并不会发生变化,缓存命中可能失效。
- 如果出现了服务器资源因为反复修改,但资源内容并没发生变化,此时浏览器再次请求服务器,实际上应该认为缓存命中(实际内容没有变化),但是此时通过该字段的比较会导致缓存命中失败
Etag/ If-None-Match(优先级最高)
为了解决Last-Modifed的缓存命中问题,可以通过Etag
来管理协商缓存命中。
- 服务端收到响应以后,根据当前资源内容重新生成一份
Etag
,比较该值和If-None-Match
是否相等,相等(内容没有发生改变)则返回304,不相等则返回200和正常响应。但同Last-Modified
的区别在于即使服务器重新生成的Etag
字段和原来的没有变化,但是因为重新生成了,304响应中同样会返回Etag
字段。
📕说说HTTP1.0/HTTP1.1/HTTP2.0的区别
HTTP1.0—无状态无连接
HTTP/1.0规定浏览器和服务器保持短暂的连接。浏览器的每次请求都需要与服务器建立一个TCP连接
- 无连接:服务器处理完成后立即断开TCP连接
- 无状态:服务器不跟踪每个客户端也不记录过去的请求
问题:
-
无法复用连接。每次发送请求的时候都需要完成一次TCP连接释放的过程,会导致网络的利用率非常低。
-
队头堵塞(head of line blocking)。由于HTTP/1.0规定下一个请求必须在前一个请求响应到达之后才能发送。如果上一个请求响应一直不到达,那么下一个请求就不发送,就到导致阻塞后面的请求。
HTTP1.1—长连接请求管道化
特点:为了解决HTTP1.0的问题,推出了HTTP1.1
- 长连接:HTTP/1.1增加了一个
Connection
字段,通过设置Keep-alive
(默认已设置)可以保持连接不断开,客户端可以通过在请求头中携带Connection:false
来告知服务器关闭TCP连接 - 支持请求管道化(pipelining):HTTP管道化让我们把先进先出队列从客户端(请求队列)迁移到服务端(响应队列),客户端可以同时发起多个请求,但是服务端还是会按序依次发送响应
问题:
虽然HTTP/1.1支持管道化,但是服务器也必须进行逐个响应的送回,依旧无法解决队头阻塞的问题
增加HTTP头:HTTP1.1
在HTTP1.0
的基础上,增加更多的请求头和响应头来完善的功能,如下:
- 引入了更多的缓存控制策略,如cache-control, If-Match, If-None-Match等缓存头来控制缓存策略
- 引入range,允许值请求资源某个部分
- 引入host,实现了在一台WEB服务器上可以在同一个IP地址和端口号上使用不同的主机名来创建多个虚拟WEB站点(一个服务器能够用来创建多个Web站点)
并且还添加了其他的请求方法:put
、delete
、options
…
HTTP2.0
二进制分帧
HTTP/2 采用二进制格式传输数据,而非 HTTP/1.x 的文本格式,二进制协议解析起来更高效。HTTP/2 将请求和响应数据分割为更小的帧,并采用二进制编码
多路复用
HTTP/2
复用TCP
连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应,避免了”队头堵塞”
- 同域名下所有通信都在单个连接上完成
- 该连接可以承载任意数量的双向数据流
- 每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。
- 多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装,这也是多路复用同时发送数据的实现条件
服务器推送
HTTP2
引入服务器推送,允许服务端推送资源给客户端。服务端可以在发送页面HTML时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应
但是,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送RST_STREAM
帧来拒收
头部压缩
HTTP/2.0通过在通讯双方各自cache
一份”首部表”来跟踪和存储之前发送的键值对,每次响应和请求只发送“首部表”中差异数据,既避免了重复header
的传输,又减小了需要传输的大小。“首部表”在连接中始终存在,由通信双方共同渐进的更新