一、javascript基础
1.执行上下文/作用域链/闭包
(1)执行上下文
- 上下文
- 全局执行上下文 创建全局window对象(浏览器环境下),this等于window。一个程序只会有一个全局上下文。
- 函数执行上下文 函数被调用时,会为该函数创建执行上下文。每个函数都有自己的执行上下文。函数上下文可以有任意多个。多次调用函数,每次调用都会创建新的执行上下文,与之前调用的上下文并不相同。
- eval函数执行上下文(前端不常用)
- 执行栈
是一种拥有LIFO(后进先出)的数据结构栈,被用来存储代码运行时创建的所有执行上下文。
当js脚本执行时,js引擎会先创建一个全局执行上下文,并把它压入当前执行栈。每当遇到函数调用,它会为该函数创建一个新的执行上下文,并压入栈顶。
引擎会先执行执行上下文位于栈顶的函数,执行完毕上下文从栈中弹出,控制流到达栈中下一个上下文。
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');
3.创建执行上下文--创建阶段
(1) this 值的决定,即我们所熟知的 This 绑定。
全局环境中this指向全局对象,浏览器环境this指向window。
函数执行上下文中this指向取决于函数是如何被调用的。
如果他被一个引用对象调用,那么this指向这个引用的对象,否则this会被设置为全局对象或者undefined(严格模式下)。
let foo = {
baz: function() {
console.log(this);
}
}
foo.baz(); // 'this' 引用 'foo', 因为 'baz' 被 // 对象 'foo' 调用
let bar = foo.baz;
bar(); // 'this' 指向全局 window 对象,因为 // 没有指定引用对象
(2) 创建词法环境组件。
词法环境是一种持有标识符—变量映射的结构。
1)词法环境的内部有两个组件:
- 环境记录器 是存储变量和函数声明的实际位置。
- 外部环境的引用 意味着它可以访问其父级词法环境(作用域)。
2)词法环境有两种类型:
- 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 `this`的值指向全局对象。
- 在函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。
3)环境记录器也有两种类型:
- 声明式环境记录器存储变量、函数和参数。
- 对象环境记录器用来定义出现在全局上下文中的变量和函数的关系。
4) 总结
- 在全局环境中,环境记录器是对象环境记录器。
- 在函数环境中,环境记录器是声明式环境记录器。
5) 注意
对于函数环境,声明式环境记录器还包含了一个传递给函数的 `arguments` 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。
(3) 创建变量环境组件。
它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。
在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(`let` 和 `const`)绑定,而后者只用来存储 `var` 变量绑定。
**变量声明提升**
创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 `undefined`(`var` 情况下),或者未初始化(`let` 和 `const` 情况下)。
这就是为什么你可以在声明之前访问 `var` 定义的变量(虽然是 `undefined`),但是在声明之前访问 `let` 和 `const` 的变量会得到一个引用错误。
4.创建执行上下文--执行阶段
在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined
(2)作用域链和闭包
2. this/call/apply/bind
(1)this
this代表函数的执行上下文!
- 在一般函数方法中使用 this 指代全局对象
function test(){
this.x = 1; //这里this就是window
console.log(this.x);
}
test(); // 1
- 作为对象方法调用,this 指代调用他的对象
var x =3;
function test(){
alert(this.x);
}
var o = {
x:1,
m:test
};
o.m(); // 1
- 作为构造函数调用,this 指代new 出的对象
function test(){
console.log(this);
}
var o = new test(); // test {}
- 箭头函数中的this 箭头函数是ES6的新特性,最重要的特点是它会捕获其所在上下文的this作为自己的this,或者说,箭头函数本身并没有this,它会沿用外部环境的this。也就是说,箭头函数内部与其外部的this是保持一致的。
(2)如何改变this指向
JavaScript 提供了call、apply、bind这三个方法,来切换/固定this的指向。
bind方法和apply、call稍有不同,bind方法返回一个新函数,以后调用了才会执行,但apply、call会立即执行。
Function.prototype.bind()
bind()方法主要就是将函数绑定到某个对象,bind()会创建一个函数,函数体内的this对象的值会被绑定到传入bind()中的第一个参数的值,例如:f.bind(obj),实际上可以理解为obj.f(),这时f函数体内的this自然指向的是obj;
function f(y, z){
return this.x + y + z;
}
var m = f.bind({x : 1}, 2);
console.log(m(3)); // 6
这里bind方法会把它的第一个实参绑定给f函数体内的this,所以这里的this即指向{x : 1}对象,从第二个参数起,会依次传递给原始函数,这里的第二个参数2,即是f函数的y参数,最后调用m(3)的时候,这里的3便是最后一个参数z了,所以执行结果为1 + 2 + 3 = 6分步处理参数的过程其实是一个典型的函数柯里化的过程(Curry)。
function fn (){
return this
};
function Test(){};
let a = fn.bind(Test);
a(); //ƒ Test(){}
每个函数都包含两个非继承而来的方法:call()方法和apply()方法。
call和apply可以用来重新定义函数的执行环境,也就是this的指向;call和apply都是为了改变某个函数运行时的context,即上下文而存在的,换句话说,就是为了改变函数体内部this的指向。
-
Function.call(obj[, param1[, param2[, [,...paramN]]]]); 调用一个对象的方法,用另一个对象替换当前对象,可以继承另外一个对象的属性。call方法可以用来代替另一个对象调用一个方法,call方法可以将一个函数的对象上下文从初始的上下文改变为obj指定的新对象,如果没有提供obj参数,那么Global对象被用于obj。
-
Function.apply(obj[, argArray]); 如果argArray不是一个有效数组或不是arguments对象,那么将导致一个TypeError,如果没有提供argArray和obj任何一个参数,那么Global对象将用作obj。
function People(name, age) {
this.name = name;
this.age = age;
}
function Student(name, age, grade) {
People.call(this, name, age);//或者People.apply(this, [name, age]);
this.grade = grade;
}
var student = new Student('小明', 21, '大三');
console.log(student.name + student.age + student.grade);//小明21大三
(3)call和apply区别
1.apply()方法接收两个参数,一个是函数运行的作用域(this),另一个是参数数组。
2.call()方法不一定接受两个参数,第一个参数也是函数运行的作用域(this),但是传递给函数的参数必须列举出来。
function People(name, age) {
this.name = name;
this.age = age;
}
function Student(age, name, grade) {
People.call(this, name, age); //People.call(this, age, name); 才对!
this.grade = grade;
}
var student = new Student('小明', 21, '大三');
console.log(student.name + student.age + student.grade);//21小明大三
People.call(this, name, age);参数顺序应该与它所属于的方法的参数顺序保持一致
(4)call和apply实现
(5)实现bind
3.原型/继承
(1)js原型
(2)原型链
原型链的经典面试题
function Foo() {
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().getName(); //1
getName(); //1
new Foo().getName(); //3
new Foo().__proto__.getName(); //3
new Foo.getName(); //2
new new Foo().getName(); //3
函数体内的预编译,发生时的四个步骤:
- 1.创建AO(Activation Object)对象,即执行期上下文(作用域)
- 2.找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
- 3.将实参和形参相统一
- 4.找函数体里面的函数声明,将函数名作为AO对象的属性名挂起来,值为函数体
全局环境的预编译,发生时的三个步骤:
- 1.创建GO(Golbal Object)对象,即全局上下文(作用域)
- 2.找形参和变量声明,将变量和形参名作为GO属性名,值为undefined
- 3.找函数体里面的函数声明,将函数名作为GO对象的属性名挂起来,值为函数体
new 关键字执行过程
function Person(name) {
this.name = name;
}
var zhangsan = new Person('张三');
上述代码中new了一个Person,这其中的过程如下:
-
1.创建一个空对象object,let obj = new Object()—创建对象新对象,就是指在栈内新建了一个obj,这个obj实际上是指的堆中对应的一个地址。
-
2.设置原型链—这里所说原型链,就是设置新建对象obj的隐式原型即_proto_属性指向构造函数Person的显示原型prototype对象,即
obj.proto = Person.prototype
- 3.改变构造函数Person的this绑定到新对象obj,并且利用call()或者是apply()来执行构造函数Person
var result = Person.call(obj)
- 4.将第三步中初始化完成后的对象地址,保存到新对象中,同时要判断构造函数Person的返回值类型,为什么要判断值类型呢?因为如果构造函数中返回this或者是基本数据类型(number数值,string字符串,Boolean布尔,null,undefined)的值时,这个时候则返回新的实例对象,如果构造函数返回的值是引用类型的,则返回的值是引用类型 这里需要注意一点的就是js中的构造函数,是不需要返回值的,所以会默认返回一个新创建的空对象obj
上题具体解析
(3)实现继承
//父类
function Animal(name, age) {
this.name = name || 'Animal'
this.age = age
this.sleep = function () {
console.log(`${this.name} is sleeping`)
}
this.hobby = []
}
Animal.prototype.eat = function (food) {
console.log(`${this.name} is eating ${food}`)
}
function FamilyAnimal(name, age, gender) {
this.name = name || 'Animal'
this.age = age
this.sayGender = function (gender) {
console.log(`${this.name} is ${gender}男生`)
}
}
//1、原型链继承[二星推荐] -- 将父类的实例作为子类的原型
function Dog(name, age) {
this.name = name
}
Dog.prototype = new Animal()
Dog.prototype.wang = function () {
console.log('wangwang~')
}
const dog = new Dog('wangcai', 3)
console.log(dog.name)//wangcai
console.log(dog.age)//undefined
console.log(dog.sleep())//wangcai is sleeping
console.log(dog.eat('apple'))//wangcai is eating apple
console.log(dog.wang())//wangwang~
console.log(dog instanceof Dog)//true
console.log(dog instanceof Animal)//true
Animal.prototype.play = function (game) {
console.log(`${this.name} is playing ${game}`)
}
console.log(dog.play('ball'))//wangcai is playing ball
dog.hobby.push('basketBall')
const dog2 = new Dog('beibei')
console.log(dog2.name)//beibei
console.log(dog2.hobby)//['basketBall']
//特点:
// 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
// 父类新增原型方法/原型属性,子类都能访问到
// 简单,易于实现
// 缺点:
// 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
// Dog.prototype.wang = function () {//TypeError: dog.wang is not a function
// console.log('wangwang~')
// }
// Dog.prototype = new Animal()
// 无法实现多继承--无法同时继承多个父类
// 来自原型对象的所有属性被所有实例共享
// dog.hobby.push('basketBall')
// const dog2 = new Dog('beibei')
// console.log(dog2.hobby)//['basketBall']
// 创建子类实例时,无法向父类构造函数传参
// function Animal(name, age) {
// this.name = name || 'Animal'
// this.age = age
// //...
// }
// function Dog(name, age) {
// this.name = name
// }
// const dog = new Dog('wangcai', 3)
// console.log(dog.name)//wangcai
// console.log(dog.age)//undefined
console.log('--------------------')
//2.构造继承[二星推荐] -- 使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function Cat(name, age, gender, like) {
Animal.call(this, name, age)
FamilyAnimal.call(this, name, age, gender)
this.name = name || 'mimi'
this.like = like
}
const cat = new Cat('erdou', 2, false)
console.log(cat.name)//erdou
console.log(cat.age)//2
console.log(cat.sleep())//erdou is sleeping
// console.log(cat.eat('fish'))//cat.eat is not a function
console.log(cat.hobby)//[]
// console.log(cat.play('maotuan'))//cat.play is not a function
console.log(cat instanceof Cat); // true
console.log(cat instanceof Animal); // false
console.log(cat.sayGender(true))//erdou is true男生
console.log(cat instanceof FamilyAnimal); // false
// 特点:
// 解决了1中,子类实例共享父类引用属性的问题
// console.log(cat.hobby)//[]
// 创建子类实例时,可以向父类传递参数
// function Cat(name, age, gender, like) {
// Animal.call(this, name, age)
// FamilyAnimal.call(this, name, age, gender)
// // ...
// }
// const cat = new Cat('erdou', 2, false)
// console.log(cat.age)//2
// 可以实现多继承(call多个父类对象)
// Animal.call(this)
// FamilyAnimal.call(this)
// 缺点:
// 实例并不是父类的实例,只是子类的实例
// console.log(cat instanceof Cat); // true
// console.log(cat instanceof Animal); // false
// 只能继承父类的实例属性和方法,不能继承原型属性/方法
// console.log(cat.eat('fish'))//cat.eat is not a function
// console.log(cat.play('maotuan'))//cat.play is not a function
// 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
console.log('--------------------')
//3、实例继承[二星推荐] -- 为父类实例添加新特性,作为子类实例返回
function Bird(name, age) {
let instance = new Animal()
instance.name = name || 'yingge'
instance.age = age
return instance
}
const bird = new Bird('xiaoniao', 1)
console.log(bird.name)//xiaoniao
console.log(bird.age)//1
console.log(bird.sleep())//xiaoniao is sleeping
console.log(bird.eat('fish'))//xiaoniao is eating fish
console.log(bird.hobby)//[]
console.log(bird.play('maotuan'))//xiaoniao is playing maotuan
console.log(bird instanceof Bird); // false
console.log(bird instanceof Animal); // true
//特点:
// 不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果
// const bird = Bird('xiaoniao') //返回一样
// 缺点:
// 实例是父类的实例,不是子类的实例
// console.log(bird instanceof Bird); // false
// console.log(bird instanceof Animal); // true
// 不支持多继承
console.log('--------------------')
//4、拷贝继承[一星推荐]
function Fish(name) {
const animal = new Animal()
for (let i in animal) {
Fish.prototype[i] = animal[i]
}
const animal2 = new FamilyAnimal()
for (let j in animal2) {
Fish.prototype[j] = animal2[j]
}
this.name = name || 'xiaoyu'
}
const fish = new Fish('yuyu', 1)
console.log(fish.name)//yuyu
console.log(fish.age)//undefined
console.log(fish.sleep())//yuyu is sleeping
console.log(fish.eat('fish'))//yuyu is eating fish
console.log(fish.hobby)//[]
console.log(fish.play('maotuan'))//yuyu is playing maotuan
console.log(fish.sayGender(true))//yuyu is true男生
console.log(fish instanceof Fish); // true
console.log(fish instanceof Animal); // false
// 特点:
// 支持多继承
// const animal2 = new FamilyAnimal()
// for (let j in animal2) {
// Fish.prototype[j] = animal2[j]
// }
// console.log(fish.sayGender(true))//yuyu is true男生
// 缺点:
// 效率较低,内存占用高(因为要拷贝父类的属性)
// 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
console.log('--------------------')
//5.组合继承[四星推荐] -- 通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Duck(name, age) {
Animal.call(this, name, age)
this.name = name || 'Tom';
}
Duck.prototype = new Animal()
Duck.prototype.constructor = Duck
const duck = new Duck('erya', 2, false)
console.log(duck.name)//erya
console.log(duck.age)//2
console.log(duck.sleep())//erya is sleeping
console.log(duck.eat('fish'))//erya is eating fish
console.log(duck.hobby)//[]
console.log(duck.play('maotuan'))//erya is playing maotuan
console.log(duck instanceof Duck); // true
console.log(duck instanceof Animal); // true
// console.log(duck.sayGender(true))//erdou is true男生
// console.log(duck instanceof FamilyAnimal); // false
// 特点:
// 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
// console.log(duck.play('maotuan'))//erya is playing maotuan
// 既是子类的实例,也是父类的实例
// console.log(duck instanceof Duck); // true
// console.log(duck instanceof Animal); // true
// 不存在引用属性共享问题
// console.log(duck.hobby)//[]
// 可传参
// function Duck(name, age) {
// Animal.call(this, name, age)
// //...
// }
// Duck.prototype = new Animal()
// Duck.prototype.constructor = Duck
// const duck = new Duck('erya', 2, false)
// console.log(duck.age)//2
// 函数可复用--通过将父类实例作为子类原型,实现函数复用
// 缺点:
// 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
console.log('--------------------')
//6.组合继承[五星推荐] -- 通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点
function Sheep(name, age) {
Animal.call(this, name, age)
this.name = name || 'tom'
}
(function () {
// 创建一个没有实例方法的类
let mSuper = function () { }
mSuper.prototype = Animal.prototype
Sheep.prototype = new mSuper()
Sheep.prototype.constructor = Sheep
})()
const sheep = new Sheep('miemie', 2, false)
console.log(sheep.name)//miemie
console.log(sheep.age)//2
console.log(sheep.sleep())//miemie is sleeping
console.log(sheep.eat('fish'))//miemie is eating fish
console.log(sheep.hobby)//[]
console.log(sheep.play('maotuan'))//miemie is playing maotuan
console.log(sheep instanceof Sheep); // true
console.log(sheep instanceof Animal); // true
4.Promise
(1)Promise
ECMAscript 6 原生提供了 Promise 对象。 Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。
特点
特点
-
对象的状态不受外界影响 (3种状态)
- Pending状态(进行中,初始状态,不是成功或失败状态。)
- Fulfilled状态(已成功,意味着操作成功完成。)
- Rejected状态(已失败,意味着操作失败。)
-
一旦状态改变就不会再变 (两种状态改变:成功或失败)
- Pending -> Fulfilled
- Pending -> Rejected 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
Promise 优缺点
有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。
Promise 也有一些缺点。首先,无法取消 Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
Promise 创建
要想创建一个 promise 对象、可以使用 new 来调用 Promise 的构造器来进行实例化。
var promise = new Promise(function(resolve, reject){
// ... some code
if (/* 异步操作成功 */) {
resolve(value);
} else {
reject(error);
}
})
Promise构造函数接受一个函数作为参数(executor 是带有 resolve 和 reject 两个参数的函数 ),该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不用自己部署。
resolve作用是将Promise对象状态由“未完成”变为“成功”,也就是Pending -> Fulfilled,在异步操作成功时调用,并将异步操作的结果作为参数传递出去;而reject函数则是将Promise对象状态由“未完成”变为“失败”,也就是Pending -> Rejected,在异步操作失败时调用,并将异步操作的结果作为参数传递出去。
属性
Promise.length
length 属性,其值总是为 1 (构造器参数的数目).
Promise.prototype
表示 Promise 构造器的原型。
方法
Promise.all(iterable)
这个方法返回一个新的 promise 对象,该 promise 对象在 iterable 参数对象里所有的 promise 对象都成功的时候才会触发成功,一旦有任何一个 iterable 里面的 promise 对象失败则立即触发该 promise 对象的失败。这个新的 promise 对象在触发成功状态以后,会把一个包含 iterable 里所有 promise 返回值的数组作为成功回调的返回值,顺序跟 iterable 的顺序保持一致;如果这个新的 promise 对象触发了失败状态,它会把 iterable 里第一个触发失败的 promise 对象的错误信息作为它的失败错误信息。Promise.all 方法常被用于处理多个 promise 对象的状态集合。(可以参考 jQuery.when 方法 — 译者注)
Promise.race(iterable)
当 iterable 参数里的任意一个子 promise 被成功或失败后,父 promise 马上也会用子 promise 的成功返回值或失败详情作为参数调用父 promise 绑定的相应句柄,并返回该 promise 对象。
Promise.reject(reason)
返回一个状态为失败的 Promise 对象,并将给定的失败信息传递给对应的处理方法
Promise.resolve(value)
返回一个状态由给定 value 决定的 Promise 对象。如果该值是 thenable(即,带有 then 方法的对象),返回的 Promise 对象的最终状态由 then 方法执行决定;否则的话(该 value 为空,基本类型或者不带 then 方法的对象), 返回的 Promise 对象状态为 fulfilled,并且将该 value 传递给对应的 then 方法。通常而言,如果你不知道一个值是否是 Promise 对象,使用 Promise.resolve(value) 来返回一个 Promise 对象,这样就能将该 value 以 Promise 对象形式使用。
Promise 原型
属性
Promise.prototype.constructor
返回被创建的实例函数。默认为 Promise 函数。
方法
Promise.prototype.catch(onRejected)
添加一个拒绝 (rejection) 回调到当前 promise, 返回一个新的 promise。当这个回调函数被调用,新 promise 将以它的返回值来 resolve,否则如果当前 promise 进入 fulfilled 状态,则以当前 promise 的完成结果作为新 promise 的完成结果。
Promise.prototype.then(onFulfilled, onRejected)
添加解决 (fulfillment) 和拒绝 (rejection) 回调到当前 promise, 返回一个新的 promise, 将以回调的返回值来 resolve.
Promise.prototype.finally(onFinally)
添加一个事件处理回调于当前 promise 对象,并且在原 promise 对象解析完毕后,返回一个新的 promise 对象。回调会在当前 promise 运行完毕后被调用,无论当前 promise 的状态是完成 (fulfilled) 还是失败 (rejected)
then
Promise实例生成后,可用then方法分别指定两种状态回调参数。then 方法可以接受两个回调函数作为参数:
- Promise对象状态改为Resolved时调用 (必选)
- Promise对象状态改为Rejected时调用 (可选)
- then 指定两种状态回调参数
const p = new Promise((resolve, reject) => {
console.log(1) //1
resolve(2)
}).then((res) => {
console.log(res) //2
}, (err) => {
console.log(err)
})
- 成功
function sleep(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(1), ms)
})
}
sleep(1000).then((res) => console.log(res))//1秒后输出1
- 失败
function sleep(ms) {
return new Promise((resolve, reject) => {
setTimeout(() => reject('失败'), ms)
})
}
sleep(1000).then((res) => console.log(res), (err) => console.log(err))//1秒后输出'失败'
then接受的函数参数是promise成功或者失败函数里面传递的!
.then()返回的是一个新的promise实例,.then(fn1)fn1中的返回的数据作为新promise的参数,想要在then里面接收,就return 出去
const p = new Promise((resolve, reject) => {
resolve(2)
}).then((res) => {
console.log(res) //2
return res
}, (err) => {
console.log(err)
})
p.then((res) => console.log('p ok', res)) //p ok 2
const p = new Promise((resolve, reject) => {
resolve(2)
}).then((res) => {
console.log(res) //2
}, (err) => {
console.log(err)
})
p.then((res) => console.log('p ok', res)) //p ok undefined
.catch(fn)
.then(null/undefined,function(){})的别名,是发生错误是的回调函数
注意:在promise执行顺序中当.then()的成功处理函数执行,导致不会执行.catch()中的函数,反之一样
.finally(function(){})
finally() 方法返回一个Promise。 "Promise 对象用于表示一个异步操作的最终完成 (或失败), 及其结果值.")。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。这为在Promise是否成功完成后都需要执行的代码提供了一种方式。
不论是执行.then()还是执行.catch(),finally都会执行
promise执行
const p = new Promise((resolve, reject) => {
console.log(1)//1
resolve(2)
console.log(5)//5
}).then((res) => {
console.log(res)//2
return res
}, (err) => {
console.log(err)
}).finally(() => { console.log('p finished') })
const p2 = new Promise((resolve, reject) => {
console.log(3)
//(1)
resolve(6)
//(2)
// resolve(p)
console.log(4)
})
p2.then((res) => console.log('p2 ok', res))
const p3 = p2.then((res) => { console.log('p2 ok', res); return res })
p.then((res) => console.log('p ok', res)).catch(err => {
console.log(err)
})
p3.then((res) => console.log('p2 okok', res))
//(1) 1、 5、 3、 4 、2 、p2 ok 6、p2 ok 6、p finished、p2 okok 6、p ok 2
//(2) 1、 5、 3、 4 、2 、p finished、p ok 2、p2 ok 2、p2 ok 2、p2 okok 2
(2)手写Promise
(3)async await
5.深浅拷贝
(1)介绍js深浅拷贝
(2)浅拷贝实现
(3)深拷贝实现
function deepClone(obj) {
if (obj) {//undefined 和 null
const nObj = typeof obj.valueOf()//所有对象都有valueOf方法,如果存在任意原始值,他就默认将对象转换为它的原始值。
if (nObj == 'object') {//数组和对象
let newObj = obj instanceof Array ? [] : {}
if (obj instanceof Array) {
obj.forEach(i => {
console.log(i)
newObj.push(typeof i == 'object' ? deepClone(i) : i)
})
} else {
for (const i in obj) {
newObj[i] = typeof obj[i] == 'object' ? deepClone(obj[i]) : obj[i]
}
}
return newObj
} else {//
return obj.valueOf()
}
} else {
return obj
}
}
const a2 = [1, 2, { a: 3, b: { c: 4 } }]
console.log(deepClone(a2))//[1, 2, { a: 3 }]
const a = { name: 1, age: 2, hobby: { ball: { basecket: 'lll' } } }
const b = console.log(deepClone(a))//{ name: 1, age: 2, hobby: { ball: 2 } }
console.log(deepClone(1), deepClone(new Number(1))) //1 1
console.log(deepClone('hello'), deepClone(new String('hello'))) //hello hello
console.log(deepClone(true), deepClone(new Boolean(true))) //true true
console.log(deepClone(null)) //null
console.log(deepClone(undefined)) //undefined
//在Date原型上定义克隆方法
Date.prototype.deepClone = function () {
return new Date(this.valueOf())
}
const c = new Date('2021')
console.log(c.deepClone())//Fri Jan 01 2021 08:00:00 GMT+0800 (中国标准时间)
//在RegExp原型上挂载克隆方法
RegExp.prototype.deepClone = function () {
let pattern = this.valueOf()
let flags = ''
flags += pattern.global ? 'g' : ''
flags += pattern.ignoreCase ? 'i' : ''
flags += pattern.mulitiline ? 'm' : ''
return new RegExp(pattern.source, flags)
}
const reg = new RegExp('/111/')
console.log(reg.deepClone())// /\/111\//
(4)实现深拷贝要注意的问题
(5)如何解决循环引用问题
6.事件机制/EventLoop
(1)如何实现一个事件的发布订阅
(2)介绍事件循环
(3)宏任务与微任务
7.函数式编程
8.servise worker
9.web worker
10.常用方法
(1)数组方法
// - 代表不影响数组,+代表影响数组
// - join():用指定的分隔符将数组每一项拼接为字符串
// + push():向数组的末尾添加新元素
// + pop():删除数组的最后一项
// + unshift():向数组首位添加新元素
// + shift():删除数组的第一项
// - slice():按照条件查找出其中的部分元素【(0,2)不包括2】
// + splice():对数组进行增删改【(0,2,'x','y', 'z'),取前两个,并加入x,y,z】
// - filter():过滤功能
// - concat():用于连接两个或多个数组
// - indexOf():检测当前值在数组中第一次出现的位置索引
// - lastIndexOf():检测当前值在数组中最后一次出现的位置索引
// - every():判断数组中每一项都是否满足条件
// - some():判断数组中是否存在满足条件的项
// - includes():判断一个数组是否包含一个指定的值
// + sort():对数组的元素进行排序
// + reverse():对数组进行倒序
// - forEach():es5及以下循环遍历数组每一项
// - map():es6循环遍历数组每一项
// - find():返回匹配的项
// - findIndex():返回匹配位置的索引
// - reduce():从数组的第一项开始遍历到最后一项,返回一个最终的值【回调方法,初始值】
// - reduceRight():从数组的最后一项开始遍历到第一项,返回一个最终的值
// - toLocaleString()、toString():将数组转换为字符串
// - entries()、keys()、values():遍历数组【返回一个遍历器对象,可以用for...of循环进行遍历】
let arr = ['a', 'b', 'c', 'd', 'e']
// let a = arr.join()//a,b,c,d,e ['a', 'b', 'c', 'd', 'e']
// let a = arr.push('f')//6 ['a', 'b', 'c', 'd', 'e','f']
// let a = arr.pop()//'e' ['a', 'b', 'c', 'd']
// let a = arr.unshift('x')// 6 ['x', 'a', 'b', 'c', 'd', 'e']
// let a = arr.shift()//'a' [ 'b', 'c', 'd', 'e']
// let a = arr.slice(0, 2)//['a', 'b'] ['a', 'b', 'c', 'd', 'e']
// let a = arr.splice(0, 2, 'x', 'y', 'z')// ['a', 'b'] ['x','y', 'z','c', 'd', 'e']
// let a = arr.filter(i => {
// return i.charCodeAt() > 100
// })//['e'] ['a', 'b', 'c', 'd', 'e']
// let a = arr.concat(['x', 'y'])//['a', 'b', 'c', 'd', 'e', 'x', 'y'] ['a', 'b', 'c', 'd', 'e']
let arr2 = ['a', 'b', 'c', 'd', 'e', 'b']
// let a = arr2.indexOf('b')//1 ['a', 'b', 'c', 'd', 'e', 'b']
// let a = arr2.lastIndexOf('b')//5 ['a', 'b', 'c', 'd', 'e', 'b']
// console.log(a, arr2)
// let a = arr.every(i => { return i.charCodeAt() > 97 })//false ['a', 'b', 'c', 'd', 'e']
// let a = arr.every(i => { return i.charCodeAt() > 96 })//true ['a', 'b', 'c', 'd', 'e']
// let a = arr.some(i => {return i.charCodeAt() > 100})//true ['a', 'b', 'c', 'd', 'e']
// let a = arr.some(i => { return i.charCodeAt() > 102 })//false ['a', 'b', 'c', 'd', 'e']
// let a = arr.includes('c')//true ['a', 'b', 'c', 'd', 'e']
// let a = arr.includes('j')//false ['a', 'b', 'c', 'd', 'e']
// let a = arr2.sort() //['a', 'b', 'b', 'c', 'd', 'e'] ['a', 'b', 'b', 'c', 'd', 'e']
// const nArr = [1, 2, 3, 5, 4, 7, 6]
// let a = nArr.sort((a, b) => { return a - b })
// console.log(a, nArr)//[1,2,3,4,5,6,7] [1,2,3,4,5,6,7]
// let b = nArr.sort((a, b) => { return b - a })
// console.log(b, nArr)//[7,6,5,4,3,2,1] [7,6,5,4,3,2,1]
// let a = arr.reverse()//['e', 'd', 'c', 'b', 'a'] ['e', 'd', 'c', 'b', 'a']
// arr.forEach(i => {console.log(i)})//a b c d e ['a', 'b', 'c', 'd', 'e']
// arr.map(i => { console.log(i) })//a b c d e ['a', 'b', 'c', 'd', 'e']
// let a = arr.find(i => i.charCodeAt() > 99)//d ['a', 'b', 'c', 'd', 'e']
// let a = arr.findIndex(i => i.charCodeAt() > 99)//3 ['a', 'b', 'c', 'd', 'e']
// callback (执行数组中每个值的函数,包含四个参数)
// 1、previousValue (上一次调用回调返回的值,或者是提供的初始值(initialValue))
// 2、currentValue (数组中当前被处理的元素)
// 3、index (当前元素在数组中的索引)
// 4、array (调用 reduce 的数组)
// initialValue (作为第一次调用 callback 的第一个参数。)
let arr3 = [1, 2, 3, 4]
// let a = arr3.reduce((prev, cur, index, arr) => {
// console.log(prev, cur, index);
// return prev + cur;
// })// 1 2 1;3 3 2;6 4 3;10 (4) [1, 2, 3, 4]
// let a = arr3.reduce((prev, cur, index, arr) => {
// console.log(prev, cur, index);
// return prev + cur;
// }, 0)//0 1 0; 1 2 1;3 3 2;6 4 3;10 (4) [1, 2, 3, 4]
// let a = arr3.reduceRight((prev, cur, index, arr) => {
// console.log(prev, cur, index);
// return prev + cur;
// })// 4 3 2;7 2 1;9 1 0;10 (4) [1, 2, 3, 4]
// console.log(a, arr3)
// let a = arr.toLocaleString()//a,b,c,d,e (5) ['a', 'b', 'c', 'd', 'e']
// let a = arr.toString()//a,b,c,d,e (5) ['a', 'b', 'c', 'd', 'e']
for (let index of arr.keys()) {
console.log(index);//0 1 2 3 4
}
for (let elem of arr.values()) {
console.log(elem);//a b c d e
}
for (let [index, elem] of arr.entries()) {
console.log(index, elem);//0 'a'; 1 'b'; 2 'c'; 3 'd';4 'e';
}
let a = arr.entries()
console.log(a.next().value)//[0, 'a']
console.log(a, arr)//Array Iterator {} (5) ['a', 'b', 'c', 'd', 'e']
数组去重
const arr = [1, 2, 3, 6, 4, 3, 4, 2, 5]
//1 indexOf
function removeDuplication(arr) {
let newArr = []
arr.map(i => {
if (newArr.indexOf(i) < 0) {
newArr.push(i)
}
})
return newArr
}
console.log(removeDuplication(arr))//[1, 2, 3, 6, 4, 5]
//2 set
console.log(Array.from(new Set(arr)))//[1, 2, 3, 6, 4, 5]
console.log([...new Set(arr)])//[1, 2, 3, 6, 4, 5]
//3 Object键值对
function removeDuplication(arr) {
let newArr = [], obj = {}
arr.map(i => {
if (!obj[i]) {
obj[i] = i
newArr.push(i)
}
})
return newArr
}
console.log(removeDuplication(arr))//[1, 2, 3, 6, 4, 5]
数组取交集
const arr = [1,2,3,3,4,5]
const arr1 = [2,3,7,6,4]
- 方法1
function mixed(arr1, arr2) {
let arr = []
for (let i = 0; i < arr1.length; i++) {
let item1 = arr1[i]
for (let j = 0; j < arr2.length; j++) {
let item2 = arr2[j]
if (item1 == item2) {
arr1[i] = arr2[j] = null;
arr.push(item1)
break
}
}
}
return arr
}
const arr = [1, 2, 3, 3, 4, 5, 3]
const arr1 = [2, 3, 7, 6, 4, 3]
const res = mixed(arr, arr1)
console.log(res)//[2,3,3,4]
- 方法2
function mixed(arr1, arr2) {
let arr = []
arr1.filter((item, index) => {
let n = arr2.indexOf(item)//取出arr2中的下标
if (n > -1) {
arr2.splice(n, 1)//取过之后删除
arr.push(item)
}
})
return arr
}
const arr = [1, 2, 3, 3, 4, 5, 3]
const arr1 = [2, 3, 7, 6, 4, 3]
const res = mixed(arr, arr1)
console.log(res)//[2,3,3,4]
- 方法3 [网上的,但是不太准确]
const arr = [1, 2, 3, 3, 4, 5, 3]
const arr1 = [2, 3, 7, 6, 4, 3]
const res = arr1.filter((val) => new Set(arr).has(val));
console.log(res)//[2,3,4,3] 【过滤短的,等长情况都可】
//并集和差集
const bingji =[...new Set([...arr, ...arr1])];//new Set([...arr, ...arr1])返回set型,在转为数组
const chaji = arr.filter((val) => !new Set(arr1).has(val));
console.log(bingji)//[1, 2, 3, 4, 5, 7, 6]
console.log(chaji)//[1,5]
(2)字符串方法
let str = 'abcde'
console.log(str.length)//5
一、js字符
// 1.charAt返回给定索引位置的字符
console.log(str.charAt(2))//c
// 2.charCodeAt查看指定索引位置的码元值
console.log(str.charCodeAt(2))//99
// 3.fromCharCode根据给定的UTF-16码元创建字符串中的字符
console.log(String.fromCharCode(0X61, 0X62, 0X63))//abc
console.log(String.fromCharCode(97, 98, 55357, 56842, 100, 101))//ab😊de
// fromCodePoint 与 fromCharCode对应,接收任意数量的码点,返回对应字符拼接起来的字符串
console.log(String.fromCodePoint(0X1F60A))//😊
console.log(String.fromCodePoint(97, 98, 128522, 100, 101))//ab😊de
// 4.codePointAt 与 charCodeAt对应,正确解析既包含单码元字符又包含代理对字符的字符串
let str1 = 'ab😊de'
console.log(str.codePointAt(2))//99
console.log(str1.codePointAt(2))//128522
console.log(str1.codePointAt(3))//56842
二、normalize()方法,通过比较字符串与其normalize()返回值,就可以知道该字符串是否已经规范化了
//四种规范化形式:NFD、NFC、NFKD、NFKC
let str2 = String.fromCharCode(0X00C5)
console.log(str2 === str2.normalize('NFD'))//false
console.log(str2 === str2.normalize('NFC'))//true
console.log(str2 === str2.normalize('NFKD'))//false
console.log(str2 === str2.normalize('NFKC'))//true
三、字符串操作方法
//1. - concat 拼接【不改变原字符串】,接收任意多个参数。
console.log('abc'.concat("def"))//abcdef
let str3 = 'hello '
console.log(str3.concat('world', '!', '!'))//hello world!!
console.log(str3)// hello
let str4 = 'hello world'
//2. - slice 1-2个参数,返回从开始位置到结束位置之前的的子字符串[startIndex,endIndex)
console.log(str4.slice(3))//lo world
console.log(str4.slice(3, 7))//lo w //[3,7)
//负数参数当作字符串长度加上负数的值
console.log(str4.slice(-3))//-3+11=8 => str4.slice(8) //rld
console.log(str4.slice(3, -4))//[3,-4+11) //lo w
console.log(str4)//hello world
//3. - substr 1-2个参数,第一个参数是起始下标,第二个参数是返回子字符串的数量,没有默认到结尾
console.log(str4.substr(3))//lo world
console.log(str4.substr(3, 7))//7个 //lo worl
//第一个负数参数当作字符串长度加上负数的值 ,第二个转为0(数量>=0)
console.log(str4.substr(-3))//-3+11=8 => str4.slice(8) //rld
console.log(str4.substr(3, -4))//[3,0个 //""
console.log(str4)//hello world
//4. - substring 返回从开始位置到结束位置之前的的子字符串[startIndex,endIndex)
console.log(str4.substring(3))//lo world
console.log(str4.substring(3, 7))//【3,7)//lo w
//将所有负数转为0
console.log(str4.substring(-3))//substring(0) //hello world
console.log(str4.substring(3, -4))//substring(3,0) => substring(0,3) //hel
console.log(str4)//hello world
四、字符串位置方法
let str5 = 'hello world'
// 在字符串中定位子字符串,找到返回下标位置,找不到返回-1,indexOf从头开始查找,lastIndexOf从末尾开始查找
// 第二个参数x 代表开始查找的位置,indexOf -> 从x向末尾查找,lastIndexOf->从x向开头查找
console.log(str5.indexOf('o'))//4
console.log(str5.lastIndexOf('o'))//7
console.log(str5.indexOf('o', 6))//7(world)
console.log(str5.indexOf('o', 8))//-1(rld)
console.log(str5.lastIndexOf('o', 6))//4(hello w)
console.log(str5.indexOf('c'))//-1
console.log(str5.lastIndexOf('c'))//-1
console.log(str5.indexOf('he'))//0
console.log(str5.lastIndexOf('he'))//0
五、字符串包含方法(es6)
//判断字符串中是否包含另一个字符串
let str6 = 'foobarbaz'
//1. 检查开始于索引0的匹配项,第二个参数可选,表示开始的位置向末尾查找,忽略开始位置之前的
console.log(str6.startsWith('foo'))//true
console.log(str6.startsWith('bar'))//false
console.log(str6.startsWith('bar', 3))//true
console.log(str6.startsWith('baz', 3))//false
//2.检查开始与索引string.length-substring.length的索引项,第二个参数可选,当作字符串末尾的位置
console.log(str6.endsWith('foo'))//false
console.log(str6.endsWith('bar'))//false
console.log(str6.endsWith('foo', 6))//false
console.log(str6.endsWith('bar', 6))//true
//3.检查整个字符串,第二个参数可选,表示开始的位置向末尾查找,忽略开始位置之前的
console.log(str6.includes('foo'))//true
console.log(str6.includes('qux'))//false
console.log(str6.includes('bar', 3))//true
console.log(str6.includes('bar', 4))//false
六、trim()方法
// - 创建字符串副本,删除前后所有空格,返回结果,原字符串不受影响
let str7 = ' abc de f '
console.log(str7.trim())//abc de f
//左边删除
console.log(str7.trimLeft())//abc de f ;
//右边删除
console.log(str7.trimRight())// abc de f;
七、repeat()
// - 接收一个整数,表示要将字符串复制多少次,最后返回拼接所有副本后的结果
let str8 = 'la'
console.log(str8.repeat(3))//lalala
console.log(str8)//la
八、padStart()、padEnd()
//复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足条件。第一个参数是长度,第二个参数是可选的填充字符串,默认为空格
//(2)提供了多个字符的字符串,会将其拼接并折断以匹配制定长度
//(3)如果长度小于或者等于原始字符串长度,返回原始字符串
let str9 = 'foo'
console.log(str9.padStart(6))//' foo'
console.log(str9.padStart(6, '?'))//'???foo'
console.log(str9.padEnd(6))//'foo '
console.log(str9.padEnd(6, 'abc'))//'fooabc'
console.log(str9.padEnd(8, 'abc'))//'fooabcab'//(2)
console.log(str9.padEnd(2))//foo //(3)
console.log(str9)//foo
九、字符串迭代与解构
//1.字符串原型上暴露了@@iterator方法,表示可以迭代字符串的每个字符。
let msg = 'abc'
let strInterator = msg[Symbol.iterator]()
console.log(strInterator.next())//{value: 'a', done: false}
console.log(strInterator.next())//{value: 'b', done: false}
console.log(strInterator.next())//{value: 'c', done: false}
console.log(strInterator.next())//{value: undefined, done: true}
//2.for of
for (const i of msg) {
console.log(i)//a b c
}
//3.解构,把字符串分割成数组
console.log([...msg])// ['a', 'b', 'c']
十、字符串大小写转换
//如果不知道涉及什么语言,最好使用toLocaleUpperCase和toLocaleLowerCase
let s = 'hello world'
console.log(s.toLocaleUpperCase())//HELLO WORLD
console.log(s.toUpperCase())//HELLO WORLD
console.log(s.toLocaleLowerCase())//hello world
console.log(s.toLowerCase())//hello world
十一、字符串模式匹配方法
//1.match() 返回第一个元素是与整个模式匹配的字符串,其余元素则是表达式中的捕获组匹配的字符串
let text = 'cat, bat, sat, fat'
let pattern = /.at/
//等价于pattern.exec(text)
console.log(pattern.exec(text))//['cat', index: 0, input: 'cat, bat, sat, fat', groups: undefined]
let matches = text.match(pattern)
console.log(matches.index)//0
console.log(matches[0])//cat
console.log(pattern.lastIndex)//0
//2.search() 返回第一个匹配位置的索引
console.log(text.search(/at/))//1
//3.replace() 参数一:正则对象或者一个字符串 参数二:一个字符串或者一个函数
//如果第一个参数是字符串那么只会替换第一个子字符串,想要替换所有子字符串,第一个参数必须是正则表达式且带有全局标记
console.log(text.replace('at', 'ond'))//cond, bat, sat, fat
console.log(text.replace(/at/g, 'ond'))//cond, bond, sond, fond
- 第二个参数是字符串的情况下,有几个特殊的符号序列,用来插入正则表达式操作的值
| 变量名 | 解释 |
|---|---|
| $$ | 代表插入一个 $ |
| $& | 插入匹配的子串, 用来引用匹配到子串 |
| $` | 插入当前匹配到的子串左边的内容 |
| $' | 插入当前匹配到的子串的右边的内容 |
| $n | 如果第一个参数是正则, 并且 n 是个小于100的正整数, 那么插入第 n 个括号匹配的字符串 |
一些 Demo 如下:
let str = 'hello world';
str.replace('o', '$$'); // "hell$ world"
str.replace('o', '-$&-'); // "hell-o- world"
str.replace('o', '$`'); // "hellhell world"
str.replace('o', '$''); // "hell world world"
str.replace(/(o)|(d)/g, '-$1-$2'); // "hell-o- w-o-rl--d", 这个结果大家好好用心理解
- 第二个参数是函数时
在这种情况下,当匹配执行后, 该函数就会执行。 函数的返回值作为替换字符串。 (注意: 上面提到的特殊替换参数在这里不能被使用。) 另外要注意的是, 如果第一个参数是正则表达式, 并且其为全局匹配模式, 那么这个方法将被多次调用, 每次匹配都会被调用。该函数的入参解释:
| 函数入参位置 | 对应解释 |
|---|---|
| match | 匹配到的字符串 |
| p1, p2, p3, ... | 同上 2, $3 |
| offset | 匹配到的字符串在原字符串中的偏移量 |
| string | 原字符串 |
该函数的精确参数取决于第一个参数是不是正则表达式, 以及该正则里指定了多少个括号子串
// 如果我们要把字符串中的单词首字母大写, 可以进行如下操作
let str = 'hello world',
reg = /(\w)(\w*)/g;
str.replace(reg, function(match, p1, p2, offset, str) {
console.log(match, p1, offset, str);
return p1.toUpperCase() + p2;
});
对于上面的函数中的入参大家可以打印一下看看, 并加以理解;
String.prototype.replace() 这个方法在字符串操作中使用非常频繁, 同时也由于其相对复杂的入参组合导致该方法非常灵活, 我们看一看在我们项目中使用它的场景; 掩码手机号 相信大家都做过, 我们看看使用 replace 如何实现:
// 假设我们有一个手机号
let tel = '13194099515',
res = '',
tmp = '****';
// 方法1:
res = tel.replace(/\d{4}(?=\d{4}$)/g, tmp); // 第一个参数是正则
// 方法2:
res = tel.replace(/(\d{3})(\d{4})/g, '$1' + tmp); // 第一个参数是正则
// 方法3:
res = tel.replace(tel.slice(3, -4), tmp); // 第一个参数是字符串
// 方法4:
res = tel.replace(/(\d{3})(\d{4})(\d{4})/g, function(match, p1, p2, p3) {
return p1 + tmp + p3;
});
十二、localeCompare()
//比较两个字符串
//按照字母表顺序,被比较的字符串排在传入的字符串前面返回负值(通常是-1,具体还要看与实际值相关的实现)
//按照字母表顺序,被比较的字符串排在传入的字符串相等,返回0
//按照字母表顺序,被比较的字符串排在传入的字符串后面返回正值(通常是1,具体还要看与实际值相关的实现)
let baseStr = 'yellow'
console.log(baseStr.localeCompare('black'))//1
console.log(baseStr.localeCompare('yellow'))//0
console.log(baseStr.localeCompare('zoo'))//-1
11.es6
Set
// 去重
const arr = [1, 2, 3, 2, 4, 5, 2, 2, 3]
console.log(new Set(arr))//Set(5) {1, 2, 3, 4, 5}
// Set 转为数组
console.log([...new Set(arr)]) //[1, 2, 3, 4, 5]
console.log(Array.from(new Set(arr))) //[1, 2, 3, 4, 5]
//Set 属性和方法
// size: 返回Set实例的成员总数。
// add(value):添加某个值,返回 Set 结构本身。
// delete (value):删除某个值,返回一个布尔值,表示删除是否成功。
// has(value):返回一个布尔值,表示该值是否为Set的成员。
// clear():清除所有成员,没有返回值。
console.log(new Set(arr).size)//5
console.log(new Set(arr).add(9))//Set(6) {1, 2, 3, 4, 5, 9}
console.log(new Set(arr).delete(1))//true
console.log(new Set(arr).delete(10))//false
console.log(new Set(arr).has(5))//true
console.log(new Set(arr).has(7))//false
console.log(new Set(arr).clear())//undefined
//遍历成员
const set = new Set(arr)
for (let i of set.keys()) {
console.log(i) //1 2 3 4 5
}
for (let i of set.values()) {
console.log(i) //1 2 3 4 5
}
for (let i of set.entries()) {
console.log(i) //[1, 1] [2, 2] [3, 3] [4, 4] [5, 5]
}
for (let i of set) {
console.log(i) //1 2 3 4 5
}
Object.is(value1,value2)
//Object.is(value1, value2)判断两个值是否是相同的值。
console.log(Object.is('foo', 'foo')); // true
console.log(Object.is(window, window)); // true
console.log(Object.is('foo', 'bar')); // false
console.log(Object.is([], [])); // false
var foo = { a: 1 };
var bar = { a: 1 };
console.log(Object.is(foo, foo)); // true
console.log(Object.is(foo, bar)); // false
console.log(Object.is(null, null)); // true
// 特例
console.log(Object.is(0, -0)); // false
console.log(Object.is(-0, -0)); // true
console.log(Object.is(NaN, 0 / 0)); // true
// 此方法的比较类似于 ‘===’ 但是还有一些不同之处,比如:
console.log(+0 === -0)//true
console.log(NaN === NaN) // false
// 使用 Object.is()
console.log(Object.is(+0, -0)) // false
console.log(Object.is(NaN, NaN)) // true
12.js类型判断
(1)typeof
适合判断基本数据类型
typeof ''; // string 有效
typeof 1; // number 有效
typeof Symbol(); // symbol 有效
typeof true; //boolean 有效
typeof undefined; //undefined 有效
typeof 42n // "bigint"
typeof NaN // "number"
typeof null; //object 无效
typeof [] ; //object 无效
typeof new Function(); // function 有效
typeof new Date(); //object 无效
typeof new RegExp(); //object 无效
(2)instanceof
instanceof 是用来判断 A 是否为 B 的实例,表达式为:A instanceof B,如果 A 是 B 的实例,则返回 true,否则返回 false。 在这里需要特别注意的是:instanceof 检测的是原型,我们用一段伪代码来模拟其内部执行过程:
instanceof (A,B) = {
var L = A.__proto__;
var R = B.prototype;
if(L === R) {
// A的内部属性 __proto__ 指向 B 的原型对象
return true;
}
return false;
}
[] instanceof Array; // true
[] instanceof Object; // true
{} instanceof Object;// true
function Person(){};
new Person() instanceof Person;
new Person instanceof Object;// true
new Date() instanceof Date;// true
new Date() instanceof Object;// true
我们发现,虽然 instanceof 能够判断出 [ ] 是Array的实例,但它认为 [ ] 也是Object的实例,为什么呢?
我们来分析一下 [ ]、Array、Object 三者之间的关系:
从 instanceof 能够判断出 [ ].proto 指向 Array.prototype,而 Array.prototype.proto 又指向了Object.prototype,最终 Object.prototype.proto 指向了null,标志着原型链的结束。因此,[]、Array、Object 就在内部形成了一条原型链:
从原型链可以看出,[] 的 proto 直接指向Array.prototype,间接指向 Object.prototype,所以按照 instanceof 的判断规则,[] 就是Object的实例。依次类推,类似的 new Date()、new Person() 也会形成一条对应的原型链 。因此,instanceof 只能用来判断两个对象是否属于实例关系 , 而不能判断一个对象实例具体属于哪种类型。
针对数组的这个问题,ES5 提供了 Array.isArray() 方法 。该方法用以确认某个对象本身是否为 Array 类型,而不区分该对象在哪个环境中创建。 Array.isArray() 本质上检测的是对象的 [[Class]] 值,[[Class]] 是对象的一个内部属性,里面包含了对象的类型信息,其格式为 [object Xxx] ,Xxx 就是对应的具体类型 。对于数组而言,[[Class]] 的值就是 [object Array] 。
(3)Object.prototype.toString.call()
toString() 是 Object 的原型方法,调用该方法,默认返回当前对象的 [[Class]] 。这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。
Object.prototype.toString.call('') ; // [object String]
Object.prototype.toString.call(1) ; // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用
为什么要使用Object原型上的toString()方法呢?
Array.toString()
// 'function Array() { [native code] }'
Function.toString()
// 'function Function() { [native code] }'
null.toString() // ERROR
undefined.toString() //ERROR
toString()是Object的原型方法【null和undefined没有toString方法】,Array、Function是Object的实例,重写了toString()方法
var fruits = ["Banana", "Orange", "Apple", "Mango"];
var x = fruits.toString();
console.log(x)//Banana,Orange,Apple,Mango
因为toString()被重写了,所以不会去原型上查找,所以如果直接查找不准确!可以删除数组的toString()再去原型链上查找!
二、css基础
1.position
2.行内元素/块元素
3.flex
(1)介绍flex
(2)如何用flex实现九宫格
(3)flex:1指什么?flex属性默认值
(4)flex-shrink和flex-basis
(5)grid
4.rem
rem和vm优缺点
rem方案的font-size是挂在哪的
rem方案移动端字体怎么处理
5.重绘回流
(1)介绍重绘回流
(2)如何避免重绘回流
6.居中/常见布局
左边固定右边自适应布局
<style>
.box {
width: 100%;
height: 500px;
}
.box .left {
width: 200px;
height: 100%;
background: red;
}
/**** 1.flex ****/
.box {
display: flex;
}
.box .right {
background: green;
flex-grow: 1;
}
/**** 2.absolute ****/
.box {
position: relative;
}
.box .right {
position: absolute;
left: 200px;
right: 0;
top: 0;
bottom: 0;
background: green;
}
/**** 3.float ****/
.box .left {
float: left;
}
.box .right {
height: 100%;
background: green;
margin-left: 200px;
}
</style>
</head>
<body>
<div class="box">
<div class="left"></div>
<div class="right"></div>
</div>
</body>
垂直水平居中布局
<style>
.box {
width: 400px;
height: 400px;
border: 1px solid red;
position: relative;
}
.con {
width: 200px;
height: 200px;
background: green;
}
/* 1 position+margin: auto */
.con {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
/* 2 position+ transform*/
.con {
position: absolute;
top: 50%;
left: 50%;
right: 0;
bottom: 0;
transform: translate(-50%, -50%);
}
/* 3 position+ margin负值*/
.con {
position: absolute;
top: 50%;
left: 50%;
right: 0;
bottom: 0;
margin-top: -100px;
margin-left: -100px;
}
/* 4 flex*/
.box {
display: flex;
justify-content: center;
align-items: center;
}
/* 5.table-cell */
.box {
display: table-cell;
vertical-align: middle;
}
.con {
margin: 0 auto;
}
</style>
</head>
<body>
<div class="box">
<div class="con"></div>
</div>
</body>
左右固定,中间自适应
- flex
body {
display: flex;
}
.center {
flex-grow: 1;
}
<body>
<div class="left"></div>
<div class="center"></div>
<div class="right"></div>
</body>
2.position
.left {
position: absolute;
top: 0;
left: 0;
}
.right {
position: absolute;
top: 0;
right: 0;
}
.center {
margin: 0 200px;
}
<div class="left"></div>
<div class="center"></div>
<div class="right"></div>
- float
/* 已浮动元素会根据自己浮动方向占位,导致被挤下去的元素绕开占位 */
.left {
float: left;
}
.right {
float: right;
}
<div class="left"></div>
<div class="right"></div>
<div class="center">2345</div>
7.层叠上下文 z-index
8. sass/less
9. BFC
Box 是 CSS 布局的对象和基本单位, 直观点来说,就是一个页面是由很多个 Box 组成的。元素的类型和 display 属性,决定了这个 Box 的类型。 不同类型的 Box, 会参与不同的 Formatting Context(一个决定如何渲染文档的容器),因此Box内的元素会以不同的方式渲染。让我们看看有哪些盒子:
- block-level box:display 属性为 block, list-item, table 的元素,会生成 block-level box。并且参与 block fomatting context;
- inline-level box:display 属性为 inline, inline-block, inline-table 的元素,会生成 inline-level box。并且参与 inline formatting context;
- run-in box: css3 中才有, 这儿先不讲了。
Formatting Context
Formatting context 是 W3C CSS2.1 规范中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素将如何定位,以及和其他元素的关系和相互作用。最常见的 Formatting context 有 Block fomatting context (简称BFC)和 Inline formatting context (简称IFC)。
BFC是一个独立的布局环境,其中的元素布局是不受外界的影响,并且在一个BFC中,块盒与行盒(行盒由一行中所有的内联元素所组成)都会垂直的沿着其父元素的边框排列。
BFC的布局规则
内部的Box会在垂直方向,一个接一个地放置。
Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠。
每个盒子(块盒与行盒)的margin box的左边,与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。
BFC的区域不会与float box重叠。
BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到面的元素。反之也如此。
计算BFC的高度时,浮动元素也参与计算。
如何创建BFC
1、float的值不是none。
2、position的值不是static或者relative。
3、display的值是inline-block、table-cell、flex、table-caption或者inline-flex
4、overflow的值不是visible
5、根元素
BFC的作用
- 1.利用BFC避免margin重叠。
p {
color: #f55;
background: yellow;
width: 200px;
line-height: 100px;
text-align: center;
margin: 30px;
}
<p>看看我的 margin是多少</p>
<p>看看我的 margin是多少</p>
两个p标签之前的margin会重叠,将改成
div {
overflow: hidden;
}
<p>看看我的 margin是多少</p>
<div>
<p>看看我的 margin是多少</p>
</div>
- 2.自适应两栏布局
body {
width: 100%;
position: relative;
}
.left {
width: 100px;
height: 150px;
float: left;
background: rgb(139, 214, 78);
text-align: center;
line-height: 150px;
font-size: 20px;
}
.right {
height: 300px;
background: rgb(170, 54, 236);
text-align: center;
line-height: 300px;
font-size: 40px;
}
<div class="left">LEFT</div>
<div class="right">RIGHT</div>
给.right加{overflow: hidden;}
- 3. 清除浮动-解决高度塌陷问题
.par {
border: 5px solid rgb(91, 243, 30);
width: 300px;
}
.child {
border: 5px solid rgb(233, 250, 84);
width: 100px;
height: 100px;
float: left;
}
<div class="par">
<div class="child"></div>
<div class="child"></div>
</div>
给.par加{overflow: hidden;}
10. 高度塌陷
产生原因:父元素不写高度时,子元素写了浮动后,父元素会发生高度塌陷(造成父元素高度为 0)
.box {
background: chartreuse;
}
.con {
width: 200px;
height: 200px;
background: red;
float: left;
}
<div class="box">
<div class="con"></div>
</div>
解决:
-
- 给父元素加{overflow: hidden;} 生成BFC
-
- 在浮动元素后面加一个空div,并对他清除浮动
.clear {
clear: both;
overflow: hidden;
height: 0;
}
<div class="box">
<div class="con"></div>
<div class="clear"></div>
</div>
- 3.给父元素添加伪类,并使用万能清除浮动法 [推荐]
.box::after {
overflow: hidden;
visibility: hidden;
height: 0;
clear: both;
content: '';
display: block;
}
1、2、3解决后:
- 4.给父元素也添加浮动 [不推荐]
.box {
background: chartreuse;
float: left;
}
4解决后:
三、VUE
1.MVVM
(1)MVVM和MVC区别
(2)ViewModel有什么好处
2.生命周期
(1)nextTick是如何实现的
(2)父子组建挂载时生命周期的顺序是怎么样的
3.数据绑定
(1)vue双向绑定如何实现【数据劫持/发布订阅】
(2)vue2中关于数组和对象数据观察时做了什么特别处理【重写数组方法】
(3)defineProperty和Proxy区别
(4)vue中的数据为什么频繁变化但是只会更新一次
4.状态管理
(1)什么是状态管理,为什么需要状态管理
(2)vuex和Redux
(3)vuex和Redux区别
(4)如何实现简单的状态管理
5.组建通信
(1)父子通信
(2)爷孙通信
(3)兄弟通信
6.Virtual DOM
(1)Virtual DOM是什么
(2)为什么需要Virtual DOM
(3)vue的Virtual DOM解决了什么问题
7.diff
(1)介绍vue的diff策略
(2)介绍react的diff策略
(3)vue的diff策略和react的diff策略有什么不同
(4)key作用
8.Vue computed/watch原理
(1)computed如何实现
(2)watch如何实现
(3)computed时可以引用其他computed如何实现
9.Vue mixin
10.为什么vue没有高阶组件
11.vue和react区别【项目选型、多种角度】
12.React
(1)React Hook
(2)React Hoc
(3)Hoc和vue mixin区别
13.vue面试题
(1)v-for 和 v-if 优先级问题
- v-for 优先级更高,在源码内部对指令进行匹配判断的时候,源码中优先判断v-for指令。
- 如果二者同时出现,每次渲染都会先执行循环再执行判断,无论判断结论如何都无法避免循环,浪费了性能。
- 避免二者同时出现,如果需要,应在循环外套一层template,在template上进行v-if的判断,内部进行循环。也可以利用computed对数据进行过滤处理。例子如下:
<div v-for="i in lists" :key="i.k">{{ i.a }}</div
data(){
return{
data: [
{ a: 1, flag: true, k: 1 },
{ a: 2, flag: false, k: 2 },
{ a: 3, flag: true, k: 3 },
{ a: 4, flag: false, k: 4 },
{ a: 5, flag: true, k: 5 },
],
}
}
computed:{
lists(){
return this.data.filter((item) => item.flag); //此时需要遍历的只有1,3,5了
}
}
(2)为什么组件中的data要是一个函数,而new Vue()根实例的data不必须是一个函数?
<div class="hello">
<A></A>
<A></A>
<A></A>
</div>
import A from "./A.vue";
export default {
name: "HelloWorld",
components: { A },
}
如果A组件的data是对象的话,那么会造成同一个组件不同实例之间data数据共享!【对象是引用关系,存的是地址,一处修改,全部修改】,data如果是函数的话,因为每一次函数调用都会创建一个全新的执行上下文,所以同一个组件之间的data数据在不同实例里会存在不同的执行上下文中,不会造成干扰。
根结点的data不必须是一个函数,是因为vue是单例模式,只new Vue()一次,在源码中根据是否有vm,去走两个不同的实例初始化过程,根实例因为存在vm,所以走了另一个过程,该过程中不会进行typeof data=='function'的判断,其他实例需要去判断。
(3)key的作用和原理
<p v-for="i in items" :key="i">{{i}}</p>
向数组['A','B','C','D','E']B、C中间插入F得到 ['A','B','F','C','D','E']。
源码中判断节点相同的方法:
function sameVnode (a, b) {
return (
a.key === b.key && (
(
a.tag === b.tag &&//标签名
a.isComment === b.isComment &&//是否为注释
isDef(a.data) === isDef(b.data) &&//data有没有发生变化
sameInputType(a, b)//是不是input类型
) || (
isTrue(a.isAsyncPlaceholder) &&
a.asyncFactory === b.asyncFactory &&
isUndef(b.asyncFactory.error)
)
)
)
}
源码路径src/core/vdom/patch.js:updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
debugger;
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
console.log('oldStartIdx,oldEndIdx,newStartIdx,newEndIdx',oldStartIdx,oldEndIdx,newStartIdx,newEndIdx)
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 旧的开始——对比——新的开始
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 旧的结束——对比——新的结束
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 旧的开始——对比——新的结束
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 旧的结束——对比——新的开始
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
// 旧的结束了,新数组还有剩余的节点直接添加创建
console.log('oldStartIdx,oldEndIdx,newStartIdx,newEndIdx----------add',oldStartIdx,oldEndIdx,newStartIdx,newEndIdx)
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
// 新的结束了,删除旧的剩余节点
console.log('oldStartIdx,oldEndIdx,newStartIdx,newEndIdx----------remove',oldStartIdx,oldEndIdx,newStartIdx,newEndIdx)
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
- 如果没有使用key,那么a.key==b.key==undefined,再判断是否都是p标签,根据程序代码就会认为是相同节点。因为原数组是['A','B','C','D','E'],新数组['A','B','F','C','D','E']。程序都认为是相同节点,会进入patchVnode方法【patchVnode过程中会执行updateChildren这个方法,他会更新所有的两个新旧的子元素】。更新逻辑一直走的是以下这句
sameVnode(oldStartVnode, newStartVnode)
// 旧的开始——对比——新的开始
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
oldStartIdx,oldEndIdx,newStartIdx,newEndIdx 0 4 0 5
oldStartIdx,oldEndIdx,newStartIdx,newEndIdx 1 4 1 5
oldStartIdx,oldEndIdx,newStartIdx,newEndIdx 2 4 2 5
oldStartIdx,oldEndIdx,newStartIdx,newEndIdx 3 4 3 5
oldStartIdx,oldEndIdx,newStartIdx,newEndIdx 4 4 4 5
oldStartIdx,oldEndIdx,newStartIdx,newEndIdx----------add 5 4 5 5
此次操作一共进行了5次循环,3次更新,1次创建
- 如果使用key,如果新旧节点不是相同节点就不会进入patchVnode方法,尝试更新5次(但是并没有做操作),1次追加。使用key减少不必要的更新。
// patch A:新旧开头都是A 走sameVnode(oldStartVnode, newStartVnode)判断条件
// log:0 4 0 5
// A B C D E
// A B F C D E
// patch B:新旧开头都是B 走sameVnode(oldStartVnode, newStartVnode)判断条件
// log:1 4 1 5
// B C D E
// B F C D E
// patch E:先走sameVnode(oldStartVnode:C, newStartVnode:F)判断条件不成立,再走sameVnode(oldEndVnode:E, newEndVnode:E) 成立
// log:2 4 2 5
// C D E
// F C D E
// patch D:先走sameVnode(oldStartVnode:C, newStartVnode:F)判断条件不成立,再走sameVnode(oldEndVnode:D, newEndVnode:D) 成立
// log:2 3 2 4
// C D
// F C D
// patch C:先走sameVnode(oldStartVnode:C, newStartVnode:F)判断条件不成立,再走sameVnode(oldEndVnode:C, newEndVnode:C) 成立
// log:2 2 2 3
// C
// F C
// oldCh空了,newCh还剩E ,创建E并插入B后面:2 1 2 2
//
// F
oldStartIdx,oldEndIdx,newStartIdx,newEndIdx 0 4 0 5
oldStartIdx,oldEndIdx,newStartIdx,newEndIdx 1 4 1 5
oldStartIdx,oldEndIdx,newStartIdx,newEndIdx 2 4 2 5
oldStartIdx,oldEndIdx,newStartIdx,newEndIdx 2 3 2 4
oldStartIdx,oldEndIdx,newStartIdx,newEndIdx 2 2 2 3
oldStartIdx,oldEndIdx,newStartIdx,newEndIdx----------add 2 1 2 2
此次操作一共进行了5次循环,0次更新,1次创建,相比不使用key少了3次更新,但是不要小看这两次,当数据比较多时量变会产生质变
- 总结 循环patch比较的时候会走到patchVode方法,
如果两个节点相等就return
否则,就走updateChildren方法
(1)没有key的时候,因为a.key==b.key==undefined,所以判断sameVnode是相等,走进patchVode,发现A==A,B==B,C!==F,所以要updateChildren去更新,下一次同理D!==C更新,E!==D更新,所以更新了三次。最后新的剩下了,所以需要增加到最后。(依然updateChildren方法中的)
(2)有key的时候,因为会进行key的判断,所以判断sameVnode是相等,走进patchVode,发现A==A,B==B,下一次循环时判断了E==E,再次D==D,再C==C,以上5次判断走进patchVode发现oldVnode===vnode,所以不做任何处理,最后剩下一个,需要找到对应位置进行追加。
(4)Diff算法
整体策略:深度优先,同层比较!
vue是怎么更新节点的?
我们先根据真实DOM生成一颗virtual DOM,当virtual DOM某个节点的数据改变后会生成一个新的Vnode,然后Vnode和oldVnode作对比,发现有不一样的地方就直接修改在真实的DOM上,然后使oldVnode的值为Vnode。
diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁。
virtual DOM和真实DOM的区别
//真实DOM
<div>
<p>123</p>
</div>
//virtual DOM -- virtual DOM是将真实的DOM的数据抽取出来,以对象的形式模拟树形结构。
var Vnode = {
tag: 'div',
children: [
{ tag: 'p', text: '123' }
]
};
diff
在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较。
<div>
<p>123</p>
</div>
<div>
<span>456</span>
</div>
上面的代码会分别比较同一层的两个div以及第二层的p和span,但是不会拿div和span作比较。在别处看到的一张很形象的图:
diff流程图
当数据发生改变时,set方法会让调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图。
patch 打补丁
function patch (oldVnode, vnode) {
// some code
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
let parentEle = api.parentNode(oEl) // 父元素
createEle(vnode) // 根据Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
oldVnode = null
}
}
// some code
return vnode
}
patch函数接收两个参数oldVnode和Vnode分别代表新的节点和之前的旧节点【VNode和oldVNode都是对象】
- 判断两节点是否值得比较,值得比较则执行
patchVnode
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
- 不值得比较则用
Vnode替换oldVnode
如果两个节点都是一样的,那么就深入检查他们的子节点。如果两个节点不一样那就说明Vnode完全被改变了,就可以直接替换oldVnode。 虽然这两个节点不一样但是他们的子节点一样怎么办?别忘了,diff可是逐层比较的,如果第一层不一样那么就不会继续深入比较第二层了。
patchVnode
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}
这个函数做了以下事情:
- 找到对应的真实dom,称为
el - 判断
Vnode和oldVnode是否指向同一个对象,如果是,那么直接return - 如果他们都有文本节点并且不相等,那么将
el的文本节点设置为Vnode的文本节点。 - 如果
oldVnode有子节点而Vnode没有,则删除el的子节点 - 如果
oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el - 如果两者都有子节点,则执行
updateChildren函数比较子节点,这一步很重要
updateChildren
updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) { // 对于vnode.key的比较,会把oldVnode = null
oldStartVnode = oldCh[++oldStartIdx]
}else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
}else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
}else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}else {
// 使用key时的比较
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
}else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
}else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
- 将
Vnode的子节点Vch和oldVnode的子节点oldCh提取出来 oldCh和vCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和vCh至少有一个已经遍历完了,就会结束比较。
图解updateChildren
下图分别是老的Vnode和新的Vnode
oldS 老开始、oldE 老结束、S 新开始、E 新结束。
现在分别对oldS、oldE、S、E两两做sameVnode比较,有四种比较方式(指针始终向中间移动)。
oldS和S比较,如果oldS==S,oldS头指针和S头指针分别+1。oldE和E比较,如果oldE==E,oldE尾指针和E尾指针分别-1。oldS和E比较,如果oldS==E,将老的vnode头部指针指向的真实dom节点移动到最后,oldS头指针+1和E尾指针-1。oldE和S比较,如果oldE==S,将老的vnode尾部指针指向的真实dom节点移动到最前,oldE尾指针-1和S头指针+1。- 如果四种匹配没有一对是成功的,那么遍历
oldChild,S挨个和他们匹配,匹配成功就在真实dom中将成功的节点移到最前面,如果依旧没有成功的,那么将S对应的节点插入到dom中对应的oldS位置,oldS和S指针向中间移动。
这个匹配过程的结束有两个条件:
oldS > oldE表示oldCh先遍历完,那么就将多余的vCh根据index添加到dom中去S > E表示vCh先遍历完,那么就在真实dom中将区间为[oldS, oldE]的多余节点删掉
vue中的diff的必要性?
一个组件(实例)只有一个watcher,组件中可能存在多个data中的key使用,在源码(lifecycle.js--mountComponent中)$mount->new watcher,为了精确的知道更新中谁发生了变化。
什么时候发生diff?--watcher被调用的时候
修改数据后,根据数据响应式会触发setter,继而通知需要更新,将watcher添加到异步更新队列中(每次EventLoop结束就出栈)。执行更新函数(执行/调用组件渲染和更新函数,重新渲染最新虚拟dom,执行更新比较)的过程。
四、VUE源码
1.发布订阅模式
2.观察者模式
3.数据响应式原理
4.双向绑定原理
5.路由hash和history模式原理
6.虚拟DOM
7.Diff算法
8.编译模版
9.组件化
10.vuex数据流管理
11.vue.js服务端渲染(SSR)
12.项目中的内容
(1)自定义指令
(2)自定义过滤器
(3)封装插件
(4)组件之间的通信
(5)computed和watch
(-)项目中的难点,有成就的部分
五、Webpack
1.webpack原理
2.webpack源码部分
3.webpack5
4.webpack使用
5.项目中webpack优化
6.其他打包工具
7.手写
(1)loader
(2)plugin
六、前端性能优化
1.打包优化
(1)webpack
1)loader
2)dll
3)happypack
4)压缩代码
5)tree shaking
6)scop hoisting
7)code splitting
(1)图片base64、cdn
2.网络优化
(1)dns
(2)cdn
(3)缓存
(4)preload/prefetch/懒加载
(5)ssr
3.代码优化
(1)loading/骨架屏
(2)web working
(3)虚拟列表
(4)懒加载
(5)dom/style批量更新
4.其他优化
(1)网站性能优化实战——从12.67s到1.06s的故事
(2)浏览器页面资源加载过程与优化
(3)聊聊前端开发中的长列表
(4)再谈前端虚拟列表的实现
(5)浅说虚拟列表的实现原理
(6)浏览器IMG图片原生懒加载loading=”lazy”实践指南
(7)用 preload 预加载页面资源
(8)App内网页启动加速实践:静态资源预加载视角
(9)腾讯HTTPS性能优化实践
(10)Preload, Prefetch And Priorities in Chrome
(11)Front-End Performance Checklist
(12)图片与视频懒加载的详细指南
(13)使用 Intersection Observer 来懒加载图片
七、前端工程化
1.webpack
(1)介绍webapck构建流程
(2)webpack和rollup相同点和不同点
(3)loader
1)常用loader有哪些
2)介绍loader实现思路
(4)plugin
1)常用plugin有哪些
2)介绍plugin实现思路
(5)webpack如何实现热更新
(6)webpack层面如何做性能优化
(7)webpack dll
(8)webpack tree-shaking
(9)webpack scope hoisting
2.babel【babel原理】
3.模版引擎【实现简单模版引擎】
4.前端发布
(1)前端页面如何发布到线上
(2)cdn
(3)增量发布
5.weex
(1)介绍weex原理
(2)为什么weex比h5快
(3)weex优缺点
八、typescript
1.介绍ts
2.ts相比于js优势
3.泛型,什么时候使用
4.interface
5.d.ts是什么
6.ts如何编译
7.namespace/module
九、网络
1.HTTP
(1)常见状态码
(2)304表示什么?与302区别
(3)http缓存策略
(4)Connection为keep-alive表示什么
2.DNS
3.TCP
(1)三次握手
(2)四次挥手
4.HTTPS
(1)https工作原理
(2)https与http区别
5.CDN
(1)CDN是什么?应用场景有哪些
(2)CDN回源是什么
6.经典问题
(1)从输入url到页面展示发生了什么
十、设计模式
1. 了解的设计模式以及应用场景
2. Vue/React中有应用什么设计模式
十一、数据结构/算法
1.数据结构
(1)介绍栈/队列/链表
(2)js实现栈/队列/链表
(3)树
2.算法
(1)常见排序算法思路和复杂度
(2)二叉树前/中/后序遍历
(3)深度优先/广度优先思路和应用场景
(4)动态规划
(5)diff
十二、安全
1.XSS
2.CSRF
3.HTTPS
4.风控策略
5.可信前端
6.前端-服务端安全策略
十三、Node.js
1.篇文章构建你的 NodeJS 知识体系
2.真-Node多线程
3.浏览器与Node的事件循环(Event Loop)有何区别?
4.聊聊 Node.js RPC
5.Understanding Streams in Node.js
6.深入理解 Node.js 进程与线程
7. 如何通过饿了么 Node.js 面试
8.字节跳动面试官:请你实现一个大文件上传和断点续传
十四、原生小程序
其他
1.git
2.前端调试方法
3.深入浅出浏览器渲染原理
4.前端开发如何独立解决跨域问题
5.探索 Serverless 中的前端开发模式
6.JavaScript与Unicode
7.九种跨域方式实现原理(完整版)
8.7分钟理解JS的节流、防抖及使用场景
9.浏览器的工作原理:新式网络浏览器幕后揭秘
10.浏览器同源策略与ajax跨域方法汇总
11.Different Types Of Observers Supported By Modern Browsers
12.NGW」前端新技术赛场:Serverless SSR 技术内幕
面试高频题
清除浮动方法
- 浮动元素下面加一个div,样式
{clear:both} - 给父元素加
{overflow:hidden}或者{overflow:auto}IE6:给父元素设置宽高或者{zoom:1} - 给浮动元素的邻接元素添加样式
{clear:both} - 添加伪类
:after{display:block;overflow:hidden;content:'';clear:both;height:0;visibility:hidden}
flex:1
是flex-grow、flex-shrink、flex-basis的简写,默认值为 0 1 auto
-
flex-grow:定义项目的`放大比例`,即使存在空间也不会放大 -
flex-shrink:定义项目的`缩小比例`,当空间不足情况会等比缩小,若定义flex-shrink:0则为不缩小 -
flex-basis:定义了在分配多余的空间,项目占据的空间
1px 问题
由于不同的手机有不同的像素密度导致的。如果移动显示屏的分辨率始终是普通屏幕的2倍,1px的边框在devicePixelRatio=2的移动显示屏下会显示成2px,所以在高清瓶下看着1px总是感觉变胖了
- 用小数来写px值 IOS8下已经支持带小数的px值, media query对应devicePixelRatio有个查询值-webkit-min-device-pixel-ratio, css可以写成这样
.border { border: 1px solid #999 }
@media screen and (-webkit-min-device-pixel-ratio: 2) {
.border { border: 0.5px solid #999 }
}
@media screen and (-webkit-min-device-pixel-ratio: 3) {
.border { border: 0.333333px solid #999 }
}
如果使用less/sass的话只是加了1句mixin
缺点: 安卓与低版本IOS不适用, 这个或许是未来的标准写法, 现在不做指望
- 伪元素 + transform 实现
原理是把原先元素的 border 去掉,然后利用 :before 或者 :after 重做 border ,并 transform 的 scale 缩小一半,原先的元素相对定位,新做的 border 绝对定位。
单条border样式设置:
.scale-1px{ position: relative; border:none; }
.scale-1px:after{
content: '';
position: absolute;
bottom: 0;
background: #000;
width: 100%;
height: 1px;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
}
优点:所有场景都能满足,支持圆角(伪元素和本体都需要加border-radius)
缺点:对于已经使用伪元素的元素(例如clearfix),可能需要多层嵌套
- border-image
@media screen and (-webkit-min-device-pixel-ratio: 2){
.border{
border: 1px solid transparent;
border-image: url(border.gif) 2 repeat;
}
}
图片可以用gif, png, base64多种格式, 以上是上下左右四条边框的写法, 需要单一边框只要定义单一边框的border, 代码比较直观.
border-image兼容性:
缺点: 对于圆角样式, 将图片放大修改成圆角也能满足需求, 但这样无形中增加了border的宽度 存在多种边框颜色或者更改的时候麻烦
- background渐变
背景渐变, 渐变在透明色和边框色中间分割, frozenUI用的就是这种方法, 借用它的上边框写法:
@media screen and (-webkit-min-device-pixel-ratio: 2){
.ui-border-t {
background-position: left top;
background-image: -webkit-gradient(linear,left bottom,left top,color-stop(0.5,transparent),color-stop(0.5,#e0e0e0),to(#e0e0e0));
}
}
这样更改颜色比border-image方便, 兼容性
缺点: 代码量大, 而且需要针对不同边框结构, frozenUI就定义9种基本样式。而且这只是背景, 这样做出来的边框实际是在原本的border空间内部的, 如果元素背景色有变化的样式, 边框线也会消失. 最后不能适应圆角样式
5.flexible.js
这是淘宝移动端采取的方案, github的地址:github.com/amfe/lib-fl…. 前面已经说过1px变粗的原因就在于一刀切的设置viewport宽度, 如果能把viewport宽度设置为实际的设备物理宽度, css里的1px不就等于实际1px长了么. flexible.js就是这样干的.
里面的scale值指的是对ideal viewport的缩放, flexible.js检测到IOS机型, 会算出scale = 1/devicePixelRatio, 然后设置viewport
metaEl = doc.createElement('meta');
metaEl.setAttribute('name', 'viewport');
metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
devicePixelRatio=2时输出meta如下, 这样viewport与ideal viewport的比是0.5, 也就与设备物理像素一致`
<meta name="viewport" content="initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no">
另外html元素上的font-size会被设置为屏幕宽的1/10, 这样css可以以rem为基础长度单位进行改写, 比如rem是28px, 原先的7px就是0.25rem. border的宽度能直接写1px.
function refreshRem() {
var width = docEl.getBoundingClientRect().width;
if (width / dpr > 540) { //大于540px可以不认为是手机屏
width = 540 * dpr;
}
var rem = width / 10;
docEl.style.fontSize = rem + 'px';
flexible.rem = win.rem = rem;
}
px和rem相互转换的计算方法会暴露在window.lib.flexible中. 这样可以为less/sass编写宏方法. 具体的css改写方法参照大漠的文章www.w3cplus.com/mobile/lib-…
项目中特别指出了为了防止字体模糊, 出现奇数字号的字体, 字体的实际单位还是要以px为单位.
缺点: 不适用安卓, flexible内部做了检测 非iOS机型还是采用传统的scale=1.0, 原因在于安卓手机不一定有devicePixelRatio属性, 就算有也不一定能响应scale小于1的viewport缩放设置, 例如我的手机设置了scale=0.33333333, 显示的结果也与scale=1无异.
综合使用
对于IOS, flexible.js处理的已经很好了, 对于Android,方法2,3,4结合起来大体可以满足要求. flexible.js虽然不适用于安卓, 但它里面的这一段代码可以用来做对安卓机的部署.
if (!dpr && !scale) {
var isAndroid = win.navigator.appVersion.match(/android/gi);
var isIPhone = win.navigator.appVersion.match(/iphone/gi);
var devicePixelRatio = win.devicePixelRatio;
if (isIPhone) {
// iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
dpr = 3;
} else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
dpr = 2;
} else {
dpr = 1;
}
} else {
// 其他设备下,仍旧使用1倍的方案
dpr = 1;
}
scale = 1 / dpr;
}
这里对安卓做检测, 如果是安卓, js动态加载css.
var link = document.createElement('link');
link.setAttribute("rel","stylesheet");
link.setAttribute("type","text/css");
link.setAttribute("href",".......Android.css");
document.querySelector('head').appendChild(link);
- 使用box-shadow模拟边框 利用css 对阴影处理的方式实现0.5px的效果
样式设置:
.box-shadow-1px {
box-shadow: inset 0px -1px 1px -1px #c8c7cc;
}
优点:代码量少,可以满足所有场景
缺点:边框有阴影,颜色变浅
画一条0.5px的线
- border-image 使用border-image每次都要去调整图片,总是需要成本的。基于上述的原因,我们可以借助于PostCSS的插件postcss-write-svg来帮助我们。如果你的项目中已经有使用PostCSS,那么只需要在项目中安装这个插件。然后在你的代码中使用:
border: 1px solid transparent;
border-image: svg(1px-border param(--color #00b1ff)) 2 2 stretch;
//
border-image: url(../img/border_img.jpg) 2 2 stretch;
使用PostCSS的postcss-write-svg插件,除了可以使用border-image来实现1px的边框效果之外,还可以使用background-image来实现。比如:
@svg square {
@rect {
fill: var(--color, black);
width: 100%;
height: 100%;
}
}
#example {
background: white svg(square param(--color #00b1ff));
}
编译出来就是:
#example {
background: white url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2300b1ff' width='100%25' height='100%25'/%3E%3C/svg%3E");
}
div{
background:black;//一定是background而非color
width:100%;
height:1px;
transform:scaleY(0.5)
}
rem与em区别
rem是基于html元素的字体大小来决定,而em则根据使用它的元素的大小决定(很多人错误以为是根据父类元素,实际上是使用它的元素继承了父类的属性才会产生的错觉)
页面生成为图片和二维码
原因:在工作中有需要将页面生成图片或者二维码的需求.可能我们第一想到的,交给后端来生成更加简单,但是这样,我们需要将页面代码全部传给后端,网络性能消耗太大.
- 方法1: 使用ORCode生成二维码
import QRcode from 'qrcode';
使用async生成图片
cosnt options = {};
const url = window.location.href;
async url =>
try {
console.log(await QRCode.toDataURL(url,options))} catch(err){
console.log(err);
将await QRCode.toDataURL(url,options)赋值给图片url即可
- 方法2:生成图片-主要是使用htmlToCanvas生成canvas画布
import html2canvas from 'html2canvas';
HTMLcanvas(document.body).then(function(canvas){
document.body.appendChild(canvas);
});
但是不单单此处就完了,由于canvas的原因,移动端生成出来的图片比较模糊
我们使用一个新的canvas方法多倍生成,放入一杯容器里面,达到更加清晰的效果,通过超链接下载图片,下载文件简单实现,更完整的实现方法之后更新
const scaleSize = 2;
const neewCanvas = document.createElement("canvas")
const target = document.querySelector('div');
const width = parseInt(window.getComputedStyle(target).width);
const height parseInt(window.getComputedStyle(target).height);
newCanvas.width = width* scaleSize;
newCanvas.height= height*scaleSize;
newCanvas.width = width + 'px'
newCanvas.style.height = height+'px'
const context = newCanvas.getContext('2d")
context.scale(scaleSize,scaleSzie);
html2canvas(document.querySelector('.demo),{canvas:newCanvas}).then(function(canvas){
//简单的通过超链接设置下载功能
document.querySelector("btn").setAttribute('href',canvas.toDataUrl()}
}
// 根据需要设置scaleSize大小
浏览器兼容问题
不同浏览器的标签默认的外补丁和内补丁不同
*{margin:0;padding:0;}
设置较小高度标签(一般小于10px),在IE6,IE7,遨游中高度超出自己设置高度
给超出高度的标签设置overflow:hidden;或者设置行高line-height 小于你设置的高度。
图片默认有间距
img {
width: 100px;
float: left;
}
外层盒子高度200,内层300,希望滚动,但外层被撑高到300了,overflow-y: scroll;
.box{
height: 200px;
// min-height:200px;
overflow-y: scroll;
}
各种属性的不同浏览器兼容写法,透明度兼容写法等
-webkit-touch-callout: none; /*系统默认菜单被禁用*/
-webkit-user-select: none; /*webkit浏览器*/
-khtml-user-select: none; /*早期浏览器*/
-moz-user-select: none; /*火狐浏览器*/
-ms-user-select: none; /*IE浏览器*/
user-select: none; /*用户是否能够选中文本*/
h5 iOS滑动不流畅
表现:上下滑动页面会产生卡顿,手指离开页面,页面立即停止运动.整体表现就是滑动不流畅,没有滑动惯性.
原因:在iOS5.0以及之后的版本,滑动有定义两个值auto和touch,默认值为auto. -webkit-overflow-scrolling:touch; 当手指从触摸屏上移开,会保持一段时间的滚动. -webkit-overflow-scrolling:auto; 当手指从触摸屏上移开,滚动会立即停止.
- 解决1: 在滚动容器上增加滚动touch方法,将-webkit-overflow-scrolling值设置为touch 设置滚动条隐藏:.container :: webkit-scrolling-scrollbar {display:none} 注意:可能会导致使用postion:fixed、固定定位的元素,随着页面一起滚动
- 解决2:
设置外部overflow为hidden,设置内容元素overflow为auto.内部元素body即产生滚动,超出部分隐藏
body{overflow-y:hidden} .wrapper{overflow-y:auto}
两者结合使用更加
- 解决3:better-scroll
h5 iOS上拉边界下拉出现白色空白
表现:手指按住 屏幕下拉,屏幕顶部会多出一块白色区域.手指按住屏幕上拉,底部多出一块白色区域.
原因:在iOS中,手指按住屏幕上下拖动,会触发touchmove事件.这个事件触发的对象时整个webview容器.容器自然会被拖动,剩下的部分会成空白
- 解决1:监听事件禁止滑动 移动端触摸事件有三个,分别定义为: touchstart:手指放在一个dom元素上 touchmove:手指拖拽一个dom元素 touchend:手指从一个dom元素上移开 touchmove事件的熟读是可以实现定义的,取决于硬件性功能和其他实现细节 preventDefalut方法,阻止同一触点上所有默认行为,比如滚动
由此,我们找到解决方案,通过监听touchmove,让需要滑动的地方滑动,不需要滑动的地方禁止滑动
值得注意的是我们要过滤掉具有滚动容器的元素
document.body.addEventListener('touchmove',(e)=>{
if(e._isScroller)
return;
//阻止默认事件
e.preventDefault();
},{
passive:false
}
)
- 解决2:滚动妥协填充空白,装饰城其他功能 在很多时候,我们可以不去解决这个问题,换一个思路,根据场景,我们可以将下拉作为一个功能性操作. 比如:下拉后刷新页面
h5 页面方法或者缩小不确定性行为
表现:双击或者双支张开手指页面元素,页面会放大或缩小
产生原因:HTML本身会产生放大或缩小的行为,比如在PC浏览器上,可以自动控制页面的放大缩小.但是在移动端,我们是不需要这个行为.所以,我们需要禁止该不确定性行为,来提升用户体验
原理以及解决方案:
HTML meta元标签中有个viewport属性,用来控制页面缩放,一般用于移动端. 移动端常规写法
<meta name="viewport" conent="width=device-width,inital-scale=1.0">
因此我们可以设置maximum-scale、minimum-scale与user-scalable=no用来避免这个问题
h5 微信字体设置大字号导致页面变形问题
<meta name="viewport"
content="width=device-width,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0,user-scalable=0">
<script>
(function () {
if (typeof WeixinJSBridge == "object" && typeof WeixinJSBridge.invoke == "function") {
handleFontSize();
} else {
if (document.addEventListener) {
document.addEventListener("WeixinJSBridgeReady", handleFontSize, false);
} else if (document.attachEvent) {
document.attachEvent("WeixinJSBridgeReady", handleFontSize);
document.attachEvent("onWeixinJSBridgeReady", handleFontSize);
}
}
function handleFontSize() {
// 设置网页字体为默认大小
WeixinJSBridge.invoke('setFontSizeCallback', {
'fontSize': 0
});
// 重写设置网页字体大小的事件
WeixinJSBridge.on('menu:setfont', function () {
WeixinJSBridge.invoke('setFontSizeCallback', {
'fontSize': 0
});
});
}
})();
</script>
h5 click点击事件延迟与穿透
表现:1.监听元素click事件,点击元素触发时间延迟约300ms. 2.点击蒙层,蒙层消失后,下层元素点击触发
产生原因 1.为什么会产生click延时?
iOS中的safari,为了实现双击缩放操作,在单击300ms之后,如果未进行第二次点击,则执行click单击操作.也就是说来判断用户行为是否双击产生的.但是在app中,无论是否需要双击缩放这种行为,click单击都会生成300ms延迟
2.为什么会产生click点击穿透?
双层元素叠加时,上上层元素上绑定touch事件,下层元素绑定click事件.由于click发生在touch之后,点击上层元素,元素消失,下层元素会触发click事件,由此生成了点击穿透的效果
原理以及解决方案
- 解决1. 前面已经介绍了,移动设备不仅支持点击,还支持几个触摸事件.那么我们现在基础思路就是同touch事件代替click事件
将click事件替换成touchstart不仅解决了click事件都延迟问题,还解决了穿透问题.因为穿透问题是在touch和click混用时产生的.
在原生中使用
el.addEventListener("touchstart",()=>{
console.log("ok")
},false)
在vue中使用
<button @touchstart="handTouchstart()">点击</button>
那么,是否可以将click事件全部替换成touchstart呢?为什么开源框架还会给出click事件呢
我们想象一种情景,同时需要点击和滑动的场景下.如果将click替换成touchstart会怎么样?
事件触发顺序:touchstart,touchmove,touchend,click.
很容易想象,在我需要touchmove滑动的时候,优先触发了touchstart的点击事件,是不是已经产生了冲突.
所以,在具有滚动的情况下,还是建议使用click处理
- 解决2: 使用fastclick库
使用npm/yarn安装后使用。
import FastClick from ‘fastclick’同样,使用fastclick库后,click延迟和穿透的问题都没了
软键盘将页面顶起来、收起来未回落问题
表现:1.Android手机中,点击input框时,键盘弹出,将页面顶起来,定制页面样式错乱. 2.移开焦点,键盘收起,键盘区域空白,未回落
产生原因:我们在app布局中会有个空白的底部.安卓一些版本中,输入弹窗出来,会将absolute和fixed定义的元素.导致可视区域变小,布局错乱.
解决方案:
1.软键盘将页面顶起来的解决方案,主要是通过监听页面高度变化,强制恢复成弹出前的高度
//记录原有的视口高度
const originalHeight = document.body.clientHeight || document.documentElement.clientHeight
window.onresize = function(){
var resizeHeight = document.documentElement.clientHeight || document.body.clientHeight;
if(resizeHeight < originalHeight){
const container = document.getElementById("container");
//例如 container.style.height = originalHeight;
}
}
-
键盘不能不会落问题 在iOS12+和wechat6.7.4+中,而在微信H5开发中是比较常见的Bug.
-
兼容原理:判断版本类型,更改滚动的可视区域
-
输入框等放在页面顶部,避免放在底部。
微信公众号分享
在微信公众号H5开发中,页面内部点击分享按钮调用SDK,方法不生效,微信拦截
解决:加一层蒙层,做分享引导。 因为页面内部点击分享按钮无法直接调用,二分享功能需要点击右上角更多来操作. 然后用户可能不知道通过右上角。
微信内video播放时变自动全屏播放
原因:
- ios:
webkit-playsinline="true" playsinline="true" x-webkit-airplay="true" x5-video-player-type="h5" x5-video-player-fullscreen="true" x5-video-ignore-metadata="true"
-
安卓:微信使用X5内核渲染,X5 video source media 对video重写了。
-
通用方案[未测试]
<video id="myVideo" src="..." poster="..." preload="no" autoplay="autoplay" loop="loop" webkit-playsinline playsinline x5-video-player-type="h5" x5-video-player-fullscreen="true" x5-video-orientation="portraint" x-webkit-airplay="true">
<source src="..." type="video/mp4" />
<img src="..." />
</video>
video标签的关键属性和作用
- webkit-playsinline / playsinline:iOS端默认视频只要一运行就会全屏播放,这个属性允许视频播放不弹全屏,维持在页面原位置播放;
- x5-video-player-type=”h5″:解除微信浏览器内video自家播放器的绑定;
- x5-video-player-fullscreen=”true”:是否支持全屏,如果设置为false则在微信浏览器里全屏那个标记不出现,一直维持video标签的尺寸播放视频;
- x5-video-orientation=”portraint” 视频方向,portraint为竖屏,landscape是横屏,x5-video-player-type=”h5″有效;
- x-webkit-airplay=”true”:iOS的airplay功能,一般用于投屏到大屏幕上,这个在H5场景下其实用不着
以上解决方案在目前大部分安卓或者iOS手机上都运行正常了,但是如果用户还是用几年前的安卓手机并且不升级微信,那我们也不用太纠结了,只能放弃这类访问者。
ios audio、video播放延迟,若不从0开始播放也会有问题。
解决:加loading遮罩层,等到有视频播放回调再开始播放。