JavaScript基础总结

基础

行内标签、块内标签

块标签:div p h1-h6 hr ul ol li dl dd dt form

​ ①支持宽高,自上而下排列

​ ②不受空格影响

​ ③一般用于其他标签的容器

​ ④默认宽度为100%(独占一行)。

行内标签:span i a b strong em sub sup u label br font

​ ①不支持宽高(宽高根据内容大小自动撑开),自左向右排列

​ ②受空格影响

​ ③不独占一行

行内块标签:img textarea input

​ ①支持宽高,自左向右排列

​ ②受空格影响

​ ③不独占一行

标签之间的转换

display:inline(转为行内元素)

​ inline-block(转为行内块元素)

​ block(转为块元素)

​ none(隐藏 不显示)

注意:当元素浮动(float)时会转化成行内块元素特点。

数据类型

基本数据类型

Undefined、Null、Boolean、Number 、String

数据封装类对象

Object 、 Array 、 Boolean 、 Number 和 String

原始类型与引用类型有什么区别?

js基本数据类型包括:undefined,null,number,boolean,string.

基本数据类型是按值访问的,就是说我们可以操作保存在变量中的实际的值

  1. 基本数据类型的值是不可变的(返回的是一个新的字符串,跟原先定义的变量并没有关系)
var s = "hello";
s.toUpperCase()//HELLO;
console.log(s)//hello
  1. 基本数据类型不可以添加属性和方法

  2. 基本数据类型的赋值是简单赋值([]=={}true)

  3. 基本数据类型的比较是值的比较

  4. 基本数据类型是存放在栈区的

js引用类型:Object,Array,Function,Data等

  1. 引用类型的值是可以改变的

  2. 引用类型可以添加属性和方法

  3. 引用类型的赋值是对象引用

  4. 引用类型的比较是引用的比较({}=={}false)

  5. 引用类型是同时保存在栈区和堆区中的

ECMAScript还提供了三个特殊的引用类型Boolean,String,Number.我们称这三个特殊的引用类型为基本包装类型,也叫包装对象

其它数据类型

Symbol(定义一个独一无二的值)
symbol使用
let s = Symbol()
let s1 = Symbol(‘foo’);

JSON

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。
它是基于 JavaScript 的一个子集。数据格式简单, 易于读写, 占用带宽小

JSON 字符串转换为 JSON 对象:

var obj =eval('('+ str +')');
var obj = str.parseJSON();
var obj = JSON.parse(str);

JSON 对象转换为 JSON 字符串:

var last=obj.toJSONString();
 var last=JSON.stringify(obj);

判断两个对象相等

obj = {a: 1,b: 2} 
obj2 = {a: 1,b: 2} 
obj3 = {a: 1,b: '2'} 
JSON.stringify(obj)==JSON.stringify(obj2);//true
JSON.stringify(obj)==JSON.stringify(obj3);//false

布尔值:

undefined,null, NaN,“”,0, false——>false

null == undefined ------> true

判断数据类型

null, undefied区别

null是一个表示空对象指针,转为数值时为0,使用typeof运算得到 “object” (程序级的、正常的或在意料之中的值的空缺)

undefined是一个表示"无"的原始值,当一个声明了一个变量未初始化时,得到的就是 undefined。转为数值时为NaN。(系统级的、出乎意料的或类似错误的值的空缺)

null == undefined  // true

采坑点:

undefined + 6  // NaN

null + 6    // 6

目前的用法

null表示 “没有对象”,即该处不应该有值。

(1)作为函数的参数,表示该函数的参数不是对象。

(2)作为对象原型链的终点。

undefined表示“缺少值”,就是此处应该有一个值,但是还没有定义。

(1)变量被声明了,但没有赋值时,就等于undefined。

(2)调用函数时,应该提供的参数没有提供,该参数就等于undefined。

(3)对象没有赋值的属性,该属性的值为undefined。

(4)函数没有返回值时,默认返回undefined。

1.typeof()

判断数据类型

typeof(number,NaN)-----number
typeof(boolean)-----boolean
typeof(string)------string
typeof(arr,object,null)----object
typeof(function)-----------function
typeof(undefined)---------undefined

2. instanceof

用来判断对象是不是某个构造函数的实例。沿着原型链找。

3.Object.prototype.toString.call()

undefined:[object Undefined]
null:[object Null]
Number :[object Number]
String:[object String]
true:[object Boolean]
[]:[object Array]
{}:[object Object]
function(){} :[object Function]

Object.create

  1. 语法:
    Object.create(proto, [propertiesObject])
    //方法创建一个新对象,使用现有的对象来提供新创建的对象的proto
  2. 参数:
  • proto : 必须。表示新建对象的原型对象,即该参数会被赋值到目标对象(即新对象,或说是最后返回的对象)的原型上。该参数可以是null对象, 函数的prototype属性 (创建空的对象时需传null , 否则会抛出TypeError异常)。
  • propertiesObject : 可选。 添加到新创建对象的可枚举属性(即其自身的属性,而不是原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()的第二个参数。
    3 返回值:
    在指定原型对象上添加新属性后的对象。

4.判断是否是数组

1.Array.isArray(arr)
2.Object.prototype.toString.call(arr) === ‘[object Array]’
3.arr instanceof Array
4.array.constructor === Array

5.字符串转数字parseInt()

parseInt(3, 8) -------------------->以八进制为基底将3转为十进制 3
parseInt(3, 2) --------------------> 以二进制为基底将3转为十进制(3不是二进制,报错NaN)
parseInt(3, 0) -------------------->NaN / 报错

任意进制转十进制
function trans(num,base){
  if(isNaN(num))return;
  if(isNaN(base))return;
  var arr=String(num).split('');
  var res=0;
  for(var i=0;i=base)return
​    res+=Math.pow(base,i)*arr[i];
  }
  return res;
}

十进制转任意进制
parseInt(num).toString(base)

6. 数组转字符串,空格分开

str = arr.join(’ ');

splice,slice,concat

slice(start,end) 返回浅拷贝数组 不改变原数组
splice(index,num) 如果删除一个元素 返回只包含该元素的数组 原数组改变
concat() 返回拼接后的数组 不改变原数组

eval(将字符串转化成代码执行)

"use strict"
var a = 123;
eval('console.log(a)'); //将字符串转化成代码执行------>123 *只有es5.0*

数组算法

数组扁平化

5种方法
递归的遍历每一项,若为数组则继续遍历,否则concat

function flatten(arr) {
    var res = [];
    arr.map(item => {
        if(Array.isArray(item)) {
            res = res.concat(flatten(item));
        } else {
            res.push(item);
        }
    });
    return res;
}

ES6:

function flatten(arr) {
    while(arr.some(item=>Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}

arr.some():遍历数组中每个元素,判断其是否满足指定函数的指定条件,返回true或者false

数组反转

function reverseArry(arr) [
	for(var i = 0;i<arr.length/2;i++){
		var temp = arr[i];
		arr[i] = arr[arr.length-1-i];
		arr[arr.length-1-i] = temp;
	}
]

利用unshift()在数组前添加元素

function  reverseArry(arr) {
     var result = [];
     for (let i = 0; i < arr.length; i++){
         result.unshift(arr[i])
     }
     return result;
 }
function reverseArray(arr) {
	 var newArr = [];
    for(var i = arr.length-1; i >= 0; i--){
        newArr.push(arr[i]);
    }
    return newArr;
}
function  reverseArry(arr) {
    var str = arr.join(' ');    //数组转字符串,空格分开
    var result = [];
    var word = '';
    for (let i = 0; i < str.length; i++){
        if (str[i] != ' '){
            word += str[i]; //提取每一组字符串
        }
        else{
            result.unshift(word);   //将字符串添加到结果前
            word = ''
        }
    }
    result.unshift(word);   // 最后一组字符串天剑
    return result;
}

数组去重

ES6 set

function unique(arr){
	return Array.from(new Set(array));
}

splice(ES5)

function unique(arr) {
	for (var i = 0; i < arr.length; i++) {
		for (var j = i + 1; j < arr.length; j++) {
			if (arr[i] == arr[j]) { //第一个等同于第二个, splice 方法删除第二个
			arr.splice(j, 1);
			j--;
			}
		}
	}
	return arr;
}

indexOf

function unique(arr){
	if(!Array.isArray){
		return false;
	}
	var array = [];
	for(var i = 0; i < arr.length; i++){
		if(arr.indexOf(arr[i) == -1){	//返回某个指定的字符串值在字符串中首次出现的位置。如果要检索的字符串值没有出现,则该方法返回 -1。
			array.push(arr[i]);
		}
	}
	return array;
}

有序数组合并

function concat(arr1,m,arr2,n){
	let len = m+n-1;
	m--;
	n--;
    while(n>0 || m>0){ 
		if(arr1[m]>=arr2[n]){
            arr1[len]=arr1[m];
            len--;
            m--;
		}else{
            arr1[len]=arr2[n];
            len--;
            n--;
		}
    }
    return arr1;
}

var a1 = [1,2,3,0,0,0];
var a2 = [2,5,6];
console.log(concat(a1,3,a2,3));

Set

ES6 新增的一种新的数据结构,类似于数组,但成员是唯一且无序的,没有重复的值。

数组去重

const s = new Set()
[1, 2, 3, 4, 3, 2, 1].forEach(x => s.add(x))
for (let i of s) {
    console.log(i)	// 1 2 3 4
}

// 去重数组的重复对象
let arr = [1, 2, 3, 2, 1, 1]
[... new Set(arr)]	// [1, 2, 3]

属性,方法

属性:constructor: 构造函数。 size:元素数量
方法:set.**

  1. add(value):新增,相当于 array里的push
  2. delete(value):存在即删除集合中value
  3. has(value):判断集合中是否存在 value
  4. clear():清空集合

遍历

  1. keys():返回一个包含集合中所有键的迭代器
  2. values():返回一个包含集合中所有值得迭代器(默认)
  3. entries():返回一个包含Set对象中所有元素得键值对迭代器
  4. forEach(callbackFn, thisArg):用于对集合成员执行callbackFn操作,如果提供了 thisArg 参数,回调中的this会是这个参数,没有返回值
let set = new Set([1, 2, 3])
console.log(set.keys())	// SetIterator {1, 2, 3}
console.log(set.values())	// SetIterator {1, 2, 3}
console.log(set.entries())	// SetIterator {1, 2, 3}

for (let item of set.keys()) {
  console.log(item);
}	// 1	2	 3
for (let item of set.entries()) {
  console.log(item);
}	// [1, 1]	[2, 2]	[3, 3]

set.forEach((value, key) => {
    console.log(key + ' : ' + value)
})	// 1 : 1	2 : 2	3 : 3
console.log([...set])	// [1, 2, 3]

map, filter

let set = new Set([1, 2, 3])
set = new Set([...set].map(item => item * 2))
console.log([...set])	// [2, 4, 6]

set = new Set([...set].filter(item => (item >= 4)))
console.log([...set])	//[4, 6]

交集,并集,差集

let set1 = new Set([1, 2, 3])
let set2 = new Set([4, 3, 2])

let intersect = new Set([...set1].filter(value => set2.has(value)))	//Set {2, 3}
let union = new Set([...set1, ...set2])	//Set {1, 2, 3, 4}
let difference = new Set([...set1].filter(value => !set2.has(value)))	//Set {1}

Map(字典)

var,let,const

var

  • 可以重复声明,没有报错和警告
  • 无法限制修改
  • 没有块级作用域, { }

let 和 const

  • 不能重复声明
  • 都是块级作用域, { } 块内声明的,块外无效
  • let 是变量,可以修改。 const 是常量,不能修改

为什么 var 可以重复声明?

因为编辑器会在判断有已经声明的同名变量时忽略var,然后直接赋值。

原理

在JS代码运行过程中:

引擎负责整个代码的编译以及运行

编译器则负责词法分析、语法分析、代码生成等工作

作用域负责维护所有的标识符(变量)。

var a = 2;
var a = 3;
a = 4;
alert(a); // 4

当我们执行上面的代码时,我们可以简单的理解为新变量分配一块儿内存,命名为a,并赋值为2,但在运行的时候编译器与引擎还会进行两项额外的操作:判断变量是否已经声明。

重复声明时:首先编译器对代码进行分析拆解,从左至右遇见var a,则编译器会询问作用域是否已经存在叫a的变量了。如果不存在,则招呼作用域声明一个新的变量a;若已经存在,则忽略 var 继续向下编译,这时 a = 2被编译成可执行的代码供引擎使用。

赋值时:引擎遇见a=2时同样会询问在当前的作用域下是否有变量a。若存在,则将a赋值为2(由于第一步编译器忽略了重复声明的var,且作用域中已经有a,所以重复声明会发生值的覆盖而不会报错);若不存在,则顺着作用域链向上查找,若最终找到了变量a则将其赋值2,若没有找到,则招呼作用域声明一个变量a并赋值为2(这就是为什么第二段代码可以正确执行且a变量为全局变量的原因,当然,在严格模式下JS会直接抛出异常:a is not defined)。

ES6中箭头函数

是es6中对函数一种全新的表示方法。

其中写法:1、若返回值只有一条语句,可以将{},return省略。多条不可省略。

2、参数0或多个,需要用括号。一个参数,可省略括号。

3、不能用于构造函数。

4、箭头函数的this是在定义的时候绑定,继承自父执行上下文中的this。

5、箭头函数无法使用 call()或 apply()来改变其运行的作用域。

柯里化

先传递一部分参数来调用函数,然后返回一个函数去调用剩下的参数。

把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

先用一个函数接收参数x,然后返回一个函数去处理参数y

优点:
参数复用
提前确认(第一参数用来判断是否进行下面操作)
延迟运行

柯里化bind

Function.prototype.bind = function (context) {
    let self = this; //此时this指向 test
    let arg = Array.prototype.slice.call(arguments, 1);// 去掉第一个,转换成数组
    return function () {
        let innerArg = Array.prototype.slice.call(arguments);// 此时arguments为传进来的参数,转换成数组
        let totalArg = arg.concat(innerArg); // 拼接bind进来的参数与bind之后调用的参数 作为test的参数
        return self.apply(context, totalArg);
    }
}

new

  1. 新建了一个空对象
  2. 对象的(__proto__)属性指向构造函数的prototype
  3. 绑定this
  4. 执行构造函数后返回这个对象

new和instanceof的内部机制

obj = new o(): obj的隐式原型链(proto)是指向o最初始的原型(prototype)的。这就是new的内部工作方式。

x instanceof y:x会一直沿着隐式原型链__proto__向上查找直到x.proto.proto…===y.prototype为止,如果找到则返回true,也就是x为y的一个实例。否则返回false,x不是y的实例。

变量提升

将声明提到作用域顶部。

在生成执行环境时,会有两个阶段。
第一个阶段是创建的阶段,JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined 。
第二个阶段,也就是代码执行阶段,我们可以直接提前使用

this指向(call,apply,bind,caller)

  1. 由 new 调用?绑定到新创建的对象。
  2. 由 call 或者 apply (或者 bind )调用?绑定到指定的对象。
  3. 由上下文对象调用?绑定到那个上下文对象。
  4. 默认:在严格模式下绑定到 undefined ,否则绑定到全局对象。

箭头函数this指向不会改变。

//undefined
(()=>{console.log(this.a)}).apply({a:'word'})

改变this指向

构造函数必须new

call(一个一个传值)

改变this指向,借用别人的函数,实现自己的功能。
Person.call( obj );里面的 call 让 person 所有的 this 都变成 obj。
Person.call(this, name, age, sex);

apply(数组传值)

改变this指向,第2位只能传一个参数arguments。
Person.call(this, [ name, age, sex] );

let name = 'Jack'
const obj = {name: 'Tom'}
function sayHi() {console.log('Hi! ' + this.name)}

sayHi() // Hi! Jack
sayHi.call(obj) // Hi! Tom

bind(返回是函数)

const newFunc = sayHi.bind(obj)
newFunc() // Hi! Tom

caller

function test() {
	demo();
}
function demo() {
	demo.caller; //demo被调用的环境 -->function test(){ demo(); }
}
test(); 

原型

是JavaScript语言中固有的特性,原型的原理是对象引用了另一个对象来获取该对象想的属性和方法。

prototype(原型):通过调用构造函数而创建的所有对象实例的原型对象
每个函数都有 一个prototype 属性指向一个原型对象,原型对象包含特定类型的所有实例共享的属性和方法。
每个对象(除null)都有 __proto__ 属性,这个属性指向了创建该对象的构造函数的原型。而原型对象也通过__proto__指向它自己的原型对象,层层向上直到Object.prototype,这样就形成了原型链。
原型链的特点:
当访问对象属性时,如果对象内部没有这个属性,就会沿着原型链一直往上找;
当修改原型时,与之相关的对象也会继承这一改变。
JavaScript基础总结_第1张图片

Person.prototype.name 这种.的写法是在原有的基础上把值改了。 改的是属性,也就是
房间里面的东西。
而 Person.prototype={name:’ cherry’ }是把原型改了,换了新的对象。 改了个房间。_proto_指向的空间不变。

构造函数、原型、实例之间的关系

每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( __proto__ ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

每个构造函数都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针(__proto__)。

Object.create(原型);
Object.prototype 是原型链的终端

作用域

在 JS 中,一共有两种作用域:(ES6 之前)

  • 全局作用域:作用于整个 script 标签内部,或者作用域一个独立的 JS 文件。
  • 函数作用域(局部作用域):作用于函数内的代码环境。

当代码在一个环境中执行时,会创建变量对象的一个作用域链。如果在当前作用域中没有查到值,就会沿着作用域链去上级作用域中查,直到查到全局作用域。

在内部作用域中可以访问到外部作用域的变量,在外部作用域中无法访问到内部作用域的变量。

块级作用域:ES6中新增了块级作用域,块级作用域可以通过let const声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:
在一个函数内部
在一个代码块(由一对花括号包裹)内部
块级作用域的特点:
声明变量不会被提升至代码块顶部
禁止重复声明

继承

A对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法
先继承,后使用想要继承,就必须要提供个父类(继承谁,提供继承的属性)
  JavaScript基础总结_第2张图片

一、原型链继承
     JavaScript基础总结_第3张图片

重点:让新实例的原型等于父类的实例。
特点:1、实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。(新实例不会继承父类实例的属性!)
缺点:1、新实例无法向父类构造函数传参。
   2、继承单一。
   3、所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)
  
二、借用构造函数继承
     JavaScript基础总结_第4张图片
重点:用.call()和.apply()将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))
特点:1、只继承了父类构造函数的属性,没有继承父类原型的属性。
   2、解决了原型链继承缺点1、2、3。
   3、可以继承多个构造函数属性(call多个)。
   4、在子实例中可向父实例传参。
缺点:1、只能继承父类构造函数的属性。
   2、无法实现构造函数的复用。(每次用每次都要重新调用)
   3、每个新实例都有父类构造函数的副本,臃肿。

三、组合继承(组合原型链继承和借用构造函数继承)(常用)
     JavaScript基础总结_第5张图片
重点:结合了两种模式的优点,传参和复用
特点:1、可以继承父类原型上的属性,可以传参,可复用。
   2、每个新实例引入的构造函数属性是私有的。
缺点:调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。

四、原型式继承
     JavaScript基础总结_第6张图片
重点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。
特点:类似于复制一个对象,用函数来包装。
缺点:1、所有实例都会继承原型上的属性。
   2、无法实现复用。(新实例属性都是后面添加的)
  
五、寄生式继承
     JavaScript基础总结_第7张图片
重点:就是给原型式继承外面套了个壳子。
优点:没有创建自定义类型,因为只是套了个壳子返回对象(这个),这个函数顺理成章就成了创建的新对象。
缺点:没用到原型,无法复用。
    
六、寄生组合式继承(常用)
  寄生:在函数内返回对象然后调用
  组合:1、函数的原型等于另一个实例。2、在函数中用apply或者call引入另一个构造函数,可传参 
     JavaScript基础总结_第8张图片
    重点:修复了组合继承的问题

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。

通常你使用只有一个方法的对象的地方,都可以使用闭包。

特点:

  1. 闭包一定有嵌套函数
  2. 内层函数一定操作了外层函数的局部变量
  3. 外层函数一定将内层函数返回外部,并且被全局变量保存
function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}
var size12 = makeSizer(12);

实现计数器

var add = (function(){

	var count = 0;

	return function(){

		return count += 1;

	}
})();
add();

用处

  1. 读取函数内部的变量
  2. 让变量始终保存在内存中

优点

  1. 防止变量污染。
  2. 结果缓存,避免多次调用函数发浪费资源耗时等情况。

缺点

消耗内存,参数和变量不会被垃圾回收机制回收。会造成内存溢出。

内存泄漏

内存泄漏指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放引发的各种问题。

  1. setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏。定时器未清除。
  2. 闭包使用不当。
  3. 全局变量

克隆

浅度克隆

遍历obj,将其依次放入obj1。实际上是拷贝地址,改变obj1,obj也变 。

function clone(origin, target) {
    var target = target || {}; //如果没定义对象则定义
    for (var prop in origin){
        target[prop] = origin[prop];
    }
    return target;
}
clone(obj, obj1)

深度克隆

//遍历对象 for(var prop in obj)
// 1、判断是不是原始值 typeof() ? = obj
// 2、判断数组还是对象 instanceof toString constructor
// 3、建立相应的数组和对象
//递归
function deepClone(obj){
	var newObj= obj instanceof Array ? []:{};
	for(var item in obj){
		var temple= typeof obj[item] == 'object' ? deepClone(obj[item]):obj[item];
		newObj[item] = temple;
}
return newObj;
}

事件循环机制(满分答案)

答题大纲

  1. 先说基本知识点:宏任务哪些,微任务哪些
  2. 说说事件循环机制过程,边说边画
  3. 说说 async / await 执行顺序注意,可以把 chrome 的优化,做法其实是违反了规范的,V8 团队的 PR 这些自信点说出来,会显得很好学,理解的很详细,很透彻
  4. 把 node 的事件循环也说一下,重复 1、2、3点,并且着重说 node v11 前后事件循环的变动

浏览器中的事件循环

JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序以外,还依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程,我们称为事件循环过程。一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。任务队列又分为 macro-task(宏任务)和 micro-task(微任务),在最新标准中,他们分别被称为 tasks 和 jobs。

macro-task(宏任务) 大概包括:

  • script(整体代码)
  • setTimeout
  • setInterval
  • setImmediate
  • I / O
  • UI render

micro-task(微任务) 大概包括:

  • process.nextTick
  • Promise.then
  • async / await (等价于 Promise.then)
  • MutationObserver(HTML5 新特性)

总体结论就是:

  • 执行宏任务
  • 然后执行宏任务产生的微任务
  • 若微任务在执行过程中产生了新的微任务,则继续执行微任务
  • 微任务执行完毕,再回到宏任务中进行下一轮循环

async / await 执行顺序

我们知道 async 会隐式返回一个 Promise 作为结果的函数,那么可以简单理解为:await 后面的函数在执行完毕后,await 会产生一个微任务(Promise.then 是微任务)。但是我们要注意微任务产生的时机,它是执行完 await 后,直接跳出 async 函数,执行其他代码(此处就是协程的运作,A暂停执行,控制权交给B)。其他代码执行完毕后,再回到 async 函数去执行剩下的代码,然后把 await 后面的代码注册到微任务队列中。例如:

console.log('script start')

async function async1() {
	await async2()
	console.log('async1 end')
}

async function async2() {
	console.log('async2 end')
}
async1()

setTimeout(function() {
	console.log('setTimeout')
}, 0)

new Promise(resolve => {
	console.log('Promise')
	resolve()
}).then(function() {
	console.log('promise1')
}).then(function() {
	console.log('promise2')
})

console.log('script end')

// 旧版输出如下,但是请继续看完本文下面的注意那里,新版有改动
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout

分析这段代码:

  1. 执行代码,输出 script start
  2. 执行 async1(),调用了 async2(),然后输出 async2 end,此时会保留 async1 的上下文,然后跳出 async1
  3. 遇到 setTimeout,产生一个宏任务
  4. 执行 Promise,输出 Promise,遇到 then,产生第一个微任务,继续执行代码,输出 script end
  5. 当前宏任务执行完毕,开始执行当前宏任务产生的微任务,输出 promise1,然后又遇到 then,产生一个新的微任务
  6. 执行微任务,输出 promise2,此时微任务队列已清空,执行权交还给 async1
  7. 执行 await 后的代码,输出 async1 end
  8. 所有微任务队列均已执行完毕,开始执行下一个宏任务,打印 setTimeout

注意

新版的 chrome 并不是像上面那样的执行顺序,它优化了 await 的执行速度,await 变得更早执行了,输出变更为:

// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout

但是这种做法其实违反了规范,但是规范也可以更改的,这是 V8 团队的一个 PR ,目前新版打印已经修改。知乎上也有相关的 讨论 。

我们可以分两种情况进行讨论

  1. 如果 await 后面直接跟的为一个变量,比如 await 1 。这种情况相当于直接把 await 后面的代码注册为一个微任务,可以简单理解为 Promise.then(await 后面的代码),然后跳出函数去执行其他的代码。

  2. 如果 await 后面跟的是一个异步函数的调用,比如上面的代码修改为:

    console.log('script start')
    
    async function async1() {
        await async2()
        console.log('async1 end')
    }
    async function async2() {
        console.log('async2 end')
        return Promise.resolve().then(()=>{
            console.log('async2 end1')
        })
    }
    async1()
    
    setTimeout(function() {
        console.log('setTimeout')
    }, 0)
    
    new Promise(resolve => {
        console.log('Promise')
        resolve()
    }).then(function() {
        console.log('promise1')
    }).then(function() {
        console.log('promise2')
    })
    
    console.log('script end')
    

    输出为:

    // script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout
    

    此时 执行完 await 并不会把 await 后面的代码注册到微任务对立中,而是执行完 await 之后,直接跳出了函数,执行其他同步代码,直到其他代码执行完毕后,再回到这里将 await 后面的代码推倒微任务队列中执行。注意,此时微任务队列中是有之前注册的其他微任务,所以这种情况会先执行其他的微任务。可以理解为 await 后面的代码会在本轮循环的最后被执行。

node 中的事件循环

同样是使用 V8 引擎的 Node.js 也同样有事件循环。事件循环是 Node 处理非阻塞 I / O 操作的机制,Node 中实现事件循环依赖的是 libuv 引擎。由于 Node 11 之后,事件循环的一些原理发生了改变,这里就以新的标准去讲,最后再列上变化点让大家了解前因后果。

宏任务和微任务

node 中也分为宏任务和微任务,其中,

macro-task(宏任务)包括:

  • setTimeout
  • setInterval
  • setImmediate
  • script(整体代码)
  • I / O 操作

micro-task(微任务)包括:

  • process.nextTick(与普通微任务有区别,在微任务队列执行之前执行)
  • Promise.then 回调

node 事件循环整体理解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mX8DyzFv-1679477731611)(null)]

图中每个框被成为事件循环机制的一个阶段,每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但是通常情况下,当事件循环进入特定的阶段时,它将执行特性该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段。

因此,上图可以简化为以下流程:

  1. 输入数据阶段(incoming data)
  2. 轮询阶段(poll)
  3. 检查阶段(check)
  4. 关闭时间回调阶段(close callback)
  5. 定时器检测阶段(timers)
  6. I / O 事件回调阶段(I / O callbacks)
  7. 闲置阶段(idle,prepare)
  8. 轮询阶段…

阶段概述

  • 定时器检测阶段(timers):本阶段执行 timers 的回调,即 setTimeout、setInterval 里面的回调函数
  • I / O 事件回调阶段(I / O callbacks):执行延迟到下一个循环迭代的 I / O 回调,即上一轮循环中未被执行的一些 I / O 回调
  • 闲置阶段(idle,prepare):仅供内部使用
  • 轮询阶段(poll):检索新的 I / O 事件;执行与 I / O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些计时器和 setImmediate 调度之外),其余情况 node 将在适当的时候在此阻塞
  • 检查阶段(check):setImmediate 回调函数将在此阶段执行
  • 关闭事件回调阶段(close callback):一些关闭的回调函数,如 socket.on('close', ...)

三大重点阶段

日常开发中绝大部分异步任务都在 poll、check、timers 这三个阶段处理,所以需要重点了解这三个阶段

timers

timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。同样,在 Node 中定时器指定的时间也不是准确时间,只是尽快执行。

poll

poll 是一个至关重要的阶段,poll 阶段的执行逻辑流程图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gogKX088-1679477731656)(null)]

如果当前已经存在定时器,而且有定期到时间了,拿出来执行,事件循环将会到 timers 阶段

如果没有定时器,回去看回调函数队列

  • 如果 poll 队列不为空,会遍历回到队列并同步执行,直到队列为空或达到系统限制
  • 如果 poll 队列为空,会有两件事发生
    • 如果 setImmediate 回调需要执行,poll 阶段将会停止并进入 check 阶段执行回调
    • 如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置,防止一直等待下去,一段时间后自动进入 check 阶段

check

check 阶段,这是一个比较简单的阶段,直接执行 setImmediate 的回调

process.nextTick

process.nextTick 是独立于事件循环的任务队列

在每一个事件循环阶段完成后会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行。

看一个例子:

setImmediate(() => {
    console.log('timeout1')
    Promise.resolve().then(() => console.log('promise resolve'))
    process.nextTick(() => console.log('next tick1'))
});
setImmediate(() => {
    console.log('timeout2')
    process.nextTick(() => console.log('next tick2'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
  • 在 node11 之前,因为每一个事件循环阶段完成后都会去检查 nextTick 队列,如果里面有任务,会让这部分任务优先于微任务执行,因此上述代码是先进入 check 阶段,执行所有 setImmediate,完成之后执行 nextTick 队列,最后执行微任务队列,因此输出为:

    // timeout1 -> timeout2 -> timeout3 -> timeout4 -> next tick1 -> next tick2 -> promise resolve
    
  • 在 node11 之后,process.nextTick 被视为是微任务的一种,因此上述代码是先进入 check 阶段,执行一个 setImmediate 宏任务,然后执行其微任务队列,在执行下一个宏任务及其微任务队列,因此输出为:

    // timeout1 -> next tick1 -> promise resolve -> timeout2 -> next tick2 -> timeout3 -> timepout4
    

node 版本差异说明

这里主要说明 node11 前后的差异,因为 node11 之后一些特性向浏览器看齐,总的变化一句话来说就是:

如果是 node11 版本一旦执行一个阶段里的一个宏任务(setTimeout、setInterval、setImmediate)就会立刻执行对应的微任务队列

timers 阶段执行时机的变化

setTimeout(() => {
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(() => {
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)
  • 如果是 node11以后的版本一旦执行到一个阶段内的一个宏任务(setTimeout、setInterval 和 setImmediate)就会立刻执行微任务队列,这和浏览器的运行方式是一致的,最后输出为:

    // timer1 -> promise1 -> timer2 -> promise2
    
  • 如果是 node11 之前的版本,要看第一个定时器执行完,第二个定时器是否在完成队列中

    • 如果第二个定时器还未在完成队列中,最后的结果为

      // timer1 -> promise1 -> timer2 -> promise2
      
    • 如果第二个定时器已经在完成队列中,最后结果为

      // timer1 -> timer2 -> promise1 -> promise2
      

check 阶段的执行时机变化

setImmediate(() => console.log('immediate1'));
setImmediate(() => {
    console.log('immediate2')
    Promise.resolve().then(() => console.log('promise resolve'))
});
setImmediate(() => console.log('immediate3'));
setImmediate(() => console.log('immediate4'));
  • 如果是 node11 后的版本,会输出

    // immediate1 -> immediate2 -> promise resolve -> immediate3 -> immediate4
    
  • 如果是 node11 前的版本,会输出

    // immediate1 -> immediate2 -> immediate3 -> immediate4 -> promise resolve
    

nextTick 队列执行时机的变化

setImmediate(() => console.log('timeout1'));
setImmediate(() => {
    console.log('timeout2')
    process.nextTick(() => console.log('next tick'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
  • 如果是 node11 后的版本,会输出

    // timeout1 -> timeout2 -> next tick -> timeout3 -> timeout4
    
  • 如果是 node11 前的版本,会输出

    // timeout1 -> timeout2 -> timeout3 -> timeout4 -> next tick
    

node 和 浏览器事件循环的主要区别

两者主要的区别在于浏览器中的微任务是在每个相应的宏任务中执行的,而 nodejs 中的微任务则是在不同阶段之间执行的。

Promise

Promise 是现代 Web 异步开发的重要组成部分,基本上目前所有的 Web 应用的异步开发手段都是通过 Promise 来实现的。

概念

所谓 Promise,就是一个容器对象,里面保存着某个未来才会结束的事件(异步事件)的结果。Promise 是一个构造函数,它有三个特点:

  1. Promise 有三个状态:pending(进行中)、fulfilled(成功)和 reject(失败),并且状态不受外部影响。
  2. 状态一旦改变就无法修改,并且状态只能从 pending 到 fulfilled 或者是 pending 到 reject。
  3. Promise 一旦创建就会立即执行,不能中途取消。

用法

在 Promise 诞生之前,Web 应用中的异步开发主要采用的是回调函数的模式(详情可以参考 node.js 各个 API),回调函数的一大缺点就是,当我们的一个异步结果需要使用另外一个异步结果时,就会产生回调嵌套,一旦这样的嵌套多了,就会变成回调地狱,十分影响代码观感。

而 Promise 的诞生一定程度上解决了这个问题,因为 Promise 是采用链式调用的方式,并且在 Promise 返回的 Promise 对象中的 then、catch 等一系列方法都会返回一个新的 Promise 对象。

then 方法

当 Promise 实例创建成功后,可以执行其原型上的 then 方法。then 方法接受两个参数,分别是当前 Promise 的成功回调和失败回调,并且这两个函数会自动返回一个新的 Promise 对象。

then 方法的成功回调如果返回的是一个 Promise 对象,那么只有当这个 Promise 对象状态发生改变之后,才会执行下一个 then。

catch 方法

Promise 原型上还有一个 catch 方法,它会捕获在它链式之前的所有未被捕获的错误(一旦前面的错误被捕获,就不会执行)。

catch 只是捕获异常,catch 并不能终止当前 Promise 的链式调用。

同样的,catch 方法也会自动返回一个新的 Promise 对象,一旦显示返回一个 Promise,那么只有当这个 Promise 对象状态发生改变时,链式调用才会继续往下走。

finally 方法

在 ES8 中新加入的方法,此方法和 then、catch 不同,它不会跟踪 Promise 的状态,即不管 Promise 最终变成了什么状态,都会执行这个方法,同时 finally 不接收任何参数。

同样的,finally 并不是链式调用的终点,它也会自动返回一个新的 Promise 对象。

其他 API

上述三个 API 是组成 Promise 的最基本的 API,除了这三个 API 以外,还有很多其他 API。

all 方法

这个方法同字面意思一样,参数接收一个 Promise 数组,返回一个新的 Promise。只有当数组中的每一个元素都成功之后,Promise 的状态才会变更为 fulfilled,否则只要有一个失败,状态都会变更为 rejected。

成功之后的结果同样是返回一个数组,里面的每个元素按顺序对应着传入的 Promise 数组。

race 方法

字面意思是竞速,参数同样是接收一个 Promise 数组,返回一个新的 Promise。数组中无论哪个元素先发生状态改变,结果就返回先完成的那个 Promise 的值。

allSettled 方法

该方法是 ES2020 新加入的方法,参数和返回值同 Promise.all 一样,区别在于该方法返回值不论元素是否成功,只要执行结束,则返回最终执行的结果。

CallBack Hell(回调地狱)

由于回调表达异步流程的方式是非线性的、非顺序的,导致代码调试困难。我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。
例如: ajax多层嵌套函数,很难处理bug

ajax('XXX1', () => {
    // callback 函数体
    ajax('XXX2', () => {
        // callback 函数体
        ajax('XXX3', () => {
            // callback 函数体
        })
    })
})

回调函数:因为javascript是单线程的,所以有些需要等待的地方,需要使用回调函数。
回调地狱:由于某些业务的问题,在代码中会一次性写会多层的回调嵌套,回调嵌套后的代码的维护难度,和无法排除bug。这个就被称作回调地狱。

回调地狱带来的负面作用有以下几点:

  • 代码臃肿
  • 可读性差
  • 耦合度过高,可维护性差
  • 代码复用性差
  • 容易滋生bug
  • 只能在回调里处理异常

如何解决:promise、async/await
Promise就是为了解决callback的问题而产生的。

promise、generator、async/await区别

1. promise ES6 原生

Promise是一个对象,用来传递异步操作的消息。可以将异步操作队列化,按照期望的顺序执行,返回符合预期的结果,可以在对象之间传递和操作promise,帮助我们处理队列。对于开发这种多层嵌套的代码很方便,降低了代码的维护难度等等。

优点:避免层层嵌套的回调函数(js是单线程,需要等待的地方会使用回调函数),解决了回调地狱的问题。
缺点:无法取消 Promise ,错误需要通过回调函数来捕获

ES6将事情划分为三种状态:pending,resolved,rejected

  • pending: 挂起,处于未决阶段,则表示这件事情还在挂起(最终的结果还没出来)
  • resolved:已出来,已决阶段的一种状态,表示整件事情已经出现结果,并且是一个可以按照正常逻辑进行下去的结果
  • rejected:已拒绝,已决阶段的一种状态,表示整件事情已经出现结果,并是一个无法安装正常逻辑进行下去的结果,通常用于表示有一个错误
ajax('XXX1')
  .then(res => {
      // 操作逻辑
      return ajax('XXX2')
  }).then(res => {
      // 操作逻辑
      return ajax('XXX3')
  }).then(res => {
      // 操作逻辑
  })

2. generator
生成器内部的代码是以自然的同步 / 顺序方式表达任务的一系列步骤

function *fetch() {
    yield ajax('XXX1', () => {})
    yield ajax('XXX2', () => {})
    yield ajax('XXX3', () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()

3. async/await

目的是简化 Promise api 的使用,并非是替代 Promise。

优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题
缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低。

async function test() {
  // 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
  // 如果有依赖性的话,其实就是解决回调地狱的例子了
  await fetch('XXX1')
  await fetch('XXX2')
  await fetch('XXX3')
}

跨域

https://juejin.im/post/6844903882083024910

概念

客户端在向服务器发送ajax请求的时候会有跨域的限制,

绕过同源策略去获取数据
广义跨域:

  1. 资源跳转: A链接、重定向、表单提交

  2. 资源嵌入: <\link>、<\script>、<\img>、<\frame>等dom标签,还有样式background:url()、@font-face()等文件外链

  3. 脚本请求: js发起的ajax请求、dom和js对象的跨域操作等

    同源策略

    同源策略是浏览器上为安全性考虑实施的非常重要的安全策略。
    同源:URL的协议、域名和端口相同
    只允许与本域下的接口交互。目的:避免恶意脚本盗号等。

    同源策略的限制:

    1. Cookie、LocalStorage 和 IndexDB 无法读取
    2. DOM 和 Js对象无法获得
    3. AJAX 请求不能发送

核心思想

允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。

跨域解决 — JSONP

HTML 中 script,img标签这样获取资源的标签是没有跨域限制的。

imghttps://blog.csdn.net/weixin_34392843/article/details/91367766

  1. 通过 ‘img’ 加载的图片,浏览器默认情况下会将其缓存起来。
  2. 当我们从 JS 的代码中创建的 ‘img’ 再去访问同一个图片时,浏览器就不会再发起新的请求,而是直接访问缓存的图片。但是由于 JS 中的 ‘img’ 设置了 crossorigin,也就意味着它将要以 CORS 的方式请求,但缓存中的图片显然不是的,所以浏览器直接就拒绝了。连网络请求都没有发起。
  3. 在 Chrome 的调试器中,在 network 面板中,我们勾选了 disable cache 选项,验证了问题确实如第 2 点所述,浏览器这时发起了请求并且 JS 的 ‘img’ 也能正常请求到图片。

jsonp的原理就是利用

服务端返回如下(返回时即执行全局函数):

handleCallback({"success": true, "user": "admin"})

ajax的核心是通过XmlHttpRequest获取非本页内容,而jsonp的核心则是动态添加。

转载:https://blog.csdn.net/hansexploration/article/details/80314948

跨域解决 —CORS

Cross-Origin Resource Sharing(跨域资源共享 ),是一种 ajax 跨域请求资源的方式。

简单请求:

  1. 在使用XMLHttpRequest发送请求时,浏览器发现不符合同源策略,会给请求头信息加一个Origin(请求来自哪个源(协议 + 域名 + 端口))
  2. 服务器根据这个Origin字段是否在许可范围之内,如果在许可范围内,确认接受请求后加一个响应头Access-Control-Allow-Origin。
  3. 浏览器判断响应头是否有Origin,如果有则处理响应并返回响应数据。
    如果没有则被XMLHttpRequest的onerror捕获。

简单跨域请求只需服务器端设置Access-Control-Allow-Origin,带cookie请求的前后端都要设置。

CORS进阶之cookie处理

为了安全,默认情况下,浏览器的cookie也是不能作为信息传递给跨域的服务器的。

10.107.98.46:8089/select/test.html跨域到10.107.98.46:8088/AngularTodoMVC/index.html。

第一步,要做的是首先浏览器上有cookie。

  • 响应头信息Set-Cookie来处理

先设置10.107.98.46:8089/select/test.html对应的nginx

add_header 'Set-Cookie' 'name=value';设置cookie信息的,键为name,值为value。

第一步,是要测试把cookie发送给跨域的服务器。

要发送带cookie的请求到跨域服务器中,只需要把withCredentials设为true即可。

var xhttp = new XMLHttpRequest();
xhttp.open("get", "http://10.107.98.46:8088/AngularTodoMVC/index.html", true);
xhttp.withCredentials=true
xhttp.send();

服务器端设置Access-Control-Allow-Credentials

add_header 'Access-Control-Allow-Origin' 'http://10.107.98.46:8089';
add_header 'Access-Control-Allow-Credentials' 'true';
echo "name = $cookie_name";服务器上获得cookie的信息

相比于JSONP的优势
JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据

扩展见:http://www.ruanyifeng.com/blog/2016/04/cors.html

同步,异步

异步加载 JS (延迟加载)的方式有哪些?

  1. defer:并行加载js文件,按照页面上script标签的顺序执行。只支持 IE。
  2. async : 并行加载js文件,下载完立即执行执行。创建 script,插入到 DOM 中,加载完毕后 callBack。

域名收敛、域名发散

域名收敛

将静态资源放在一个域名下。减少DNS解析的开销。

域名发散

将静态资源放在多个子域名下,就可以多线程下载,提高并行度,使客户端加载静态资源更加迅速。

域名发散是pc端为了利用浏览器的多线程并行下载能力。
域名收敛多用与移动端,提高性能,因为dns解析是是从后向前迭代解析,如果域名过多性能会下降,增加DNS的解析开销。

事件

event.target:返回触发事件的元素
event.currentTarget:返回绑定事件的元素

事件处理函数中的this指向是中为currentTarget

currentTarget和target,有时候是同一个元素,有时候不是同一个元素 (因为事件冒泡)

  • 当事件是子元素触发时,currentTarget为绑定事件的元素,target为子元素
  • 当事件是元素自身触发时,currentTarget和target为同一个元素

事件绑定的方式

  1. 嵌入dom(DOM1)
<button onclick="func()">按钮</button>
  1. 直接绑定
btn.onclick = function(){}
  1. 事件监听(可以冒泡和捕获,DOM2)
btn.addEventListener('click',function(){})

//解绑
btn.removeEventListener('click', function(){})
延伸:DOM3事件
 1. UI事件:当用户与页面上的元素交互时触发,如:load、scroll 焦点事件:当元素获得或失去焦点时触发,如:blur、focus
 2. 鼠标事件:当用户通过鼠标在页面执行操作时触发如:dbclick、mouseup
 3. 滚轮事件:当使用鼠标滚轮或类似设备时触发,如:mousewheel 文本事件:当在文档中输入文本时触发,如:textInput
 4. 键盘事件:当用户通过键盘在页面上执行操作时触发,如:keydown、keypress
 5. 合成事件:当为IME(输入法编辑器)输入字符时触发,如:compositionstart
 6. 变动事件:当底层DOM结构发生变化时触发,如:DOMsubtreeModified

事件流

事件流描述的是从页面中接收事件的顺序

DOM2事件流:

  1. 事件捕获阶段
  2. 处于目标阶段
  3. 事件冒泡阶段

事件冒泡

结构上(非视觉上)嵌套关系的元素,会存在事件冒泡的功能,即同一事件,自子元素冒泡向父元素触发。(自底向上)

阻止冒泡:
W3C:stopPropagation()
IE:cancelBubble = true;

<div class="wrapper">
    <div class="content">
        <div class="box"></div>
    </div>
</div>
<script>
	var wrapper = document.getElementsByClassName('wrapper')[0];
    var content = document.getElementsByClassName('content')[0];
    var box = document.getElementsByClassName('box')[0];
	//都是click事件
    wrapper.addEventListener('click', function () {
        console.log('wrapper')
    },false);
    content.addEventListener('click', function () {
        console.log('content')
    },false);
    box.addEventListener('click', function () {
        console.log('box')
    },false);
</script>

JavaScript基础总结_第9张图片

事件捕获

父级元素先触发,子级元素后触发。(与冒泡相反)
wrapper,content,box

阻止捕获:
W3C:preventDefault()
IE:window.event.returnValue = false

事件委托

利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。具体只需把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。(鼠标事件,键盘事件)

<div class="all">
    <div class="box">box1</div>
    <div class="box">box2</div>
    <div class="box">box3</div>
</div>

document.querySelector('.all').onclick = (event) => {
    let target = event.target
    if (target.className === 'box') {
        console.log(target.innerHTML)
    }
}

JavaScript基础总结_第10张图片

事件循环

事件循环是一个单线程循环,用于监视调用堆栈并检查是否有工作即将在任务队列中完成。如果调用堆栈为空并且任务队列中有回调函数,则将回调函数出队并推送到调用堆栈中执行。

自定义事件

MDN

var event = new Event('build');

// Listen for the event.
elem.addEventListener('build', function (e) { ... }, false);

// Dispatch the event.
elem.dispatchEvent(event);

鼠标事件

mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是mouseout

mouseenter:当鼠标移除元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是mouseleave

页面某个节点的拖曳

 给需要拖拽的节点绑定 mousedown , mousemove , mouseup 事件
 mousedown 事件触发后,开始拖拽
 mousemove 时,需要通过 event.clientX 和 clientY 获取拖拽位置,并实时更新位置
 mouseup 时,拖拽结束
 需要注意浏览器边界的情况

精确获取页面元素位置的方式有哪些?

var X= ele.getBoundingClientRect().left;

var Y =ele.getBoundingClientRect().top;

//再加上滚动距离,就可以得到绝对位置

var X= ele.getBoundingClientRect().left+document.documentElement.scrollLeft;

var Y =ele.getBoundingClientRect().top+document.documentElement.scrollTop;

防抖,节流

防抖:函数频繁触发,当停止触发后空闲一段时间后执行一次。

function debounce(fn, interval = 300) {
    let timeout = null;
    return function () {
        clearTimeout(timeout);
        timeout = setTimeout(() => {
            fn.apply(this, arguments);
        }, interval);
    };
}

节流:指定时间间隔内只会执行一次任务。

function throttle(fn, interval = 300) {
    let canRun = true;
    return function () {
        if (!canRun) return;
        canRun = false;
        setTimeout(() => {
            fn.apply(this, arguments);
            canRun = true;		//可以执行下一次的循环了
        }, interval);
    };
}

get,post

get:用户获取数据,可以不用每次都与数据库连接,所以可以使用缓存。

post:用于修改和删除的工作,所以必须与数据库交互,所以不能使用缓存。因此get请求适合于请求缓存。

cookie, localstorage, sessionstorage

Cookie

Cookie 是小甜饼的意思。顾名思义,cookie 确实非常小,它的大小限制为4KB左右。它的主要用途有保存登录信息,比如你登录某个网站市场可以看到“记住密码”,这通常就是通过在 Cookie 中存入一段辨别用户身份的数据来实现的。

localStorage

localStorage 是 HTML5 标准中新加入的技术,它并不是什么划时代的新东西。早在 IE 6 时代,就有一个叫 userData 的东西用于本地存储,而当时考虑到浏览器兼容性,更通用的方案是使用 Flash。而如今,localStorage 被大多数浏览器所支持,如果你的网站需要支持 IE6+,那以 userData 作为你的 polyfill 的方案是种不错的选择。

sessionStorage

sessionStorage 与 localStorage 的接口类似,但保存数据的生命周期与 localStorage 不同。做过后端开发的同学应该知道 Session 这个词的意思,直译过来是“会话”。而 sessionStorage 是一个前端的概念,它只是可以将一部分数据在当前会话中保存下来,刷新页面数据依旧存在。但当页面关闭后,sessionStorage 中的数据就会被清空。

重绘、重排

重绘

当一个元素视觉表现属性改变时,会触发重绘。例如元素背景颜色的改变、字体颜色的改变、边框颜色的改变、透明度的改变等。

重排(回流)

当渲染树的一部分或全部更新而导致网页结构或节点尺寸发生改变时,都会导致重排。例如可见元素节点的添加和删除、改变元素的尺寸(元素宽、高、内边距大小、边框大小)、浏览器窗口大小发生改变等。

概念:
通过渲染树中渲染对象的信息,计算出每个渲染对象的位置和尺寸,将其安排在浏览器窗口的正确位置,而有些时候我们会在文档布局完成后对文档布局进行修改,这时候可可能要重新布局,称其“回流”。

每个页面至少需要一次回流,就是在页面第一次加载的时候。

重排一定会重绘

减小重绘和重排的影响

1.改变样式时:
可以使用cssText进行优化 :

//覆盖
 element.style.cssText = "border-left:1px;border-right:2px;border-bottom:3px;";
 //添加
 element.style.cssText += ";border-left:1px;border-right:2px;border-bottom:3px;";

2.批量修改DOM:
(1)使元素脱离文档流(2次重排)
① display:block/none(页面会闪动)
② 使用文档片段(document fragement)在当前DOM之外构建一个子树,再把它拷贝回文档

let fragment = document.createDocumentFragment();
//这里对fragment进行一些节点添加操作
document.querySelector("#mytable").appendChild(fragment);
上述代码触发了一次重排并且只访问了一次DOM

③ 为需要的节点创建一个备份(cloneNode(true),拷贝所有后代,表示深拷贝),然后对副本进行操作,操作完成后,就用新的节点替代旧的节点。

let old = document.querySelector("#mylist");
let clone = old.cloneNode(true);//拷贝所有后代
//这里对clone这个节点进行若干操作
old.parentNode.replaceChild(clone,old);//通过old的父节点找到old然后将old替换成clone

(2)对其应用多重改变。(1次重排)
(3)把元素带回文档中(2次重排)

图片的懒加载和预加载

预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。

懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。

两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。
懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

文件断点续传

  1. 文件识别
    文件上传服务器前,本地和服务器“握手”,告诉服务器文件信息和切片大小。
  2. 文件切片(字节)
var packet = file.slice(start, end);
  1. 文件传输
    本地将每一块文件传输至后台,本地和服务器标识。若文件传输中断可通过标识续传。

垃圾回收

概念

没有被应用的对象就是垃圾,就要被清除。
若几个对象形成一个环,但是根访问不到,也是垃圾。

根(不可删除)

  1. 本地函数和嵌套调用链上的局部变量和参数
  2. 全局变量

必要性

由于字符串、对象和数组没有固定大小,所有当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript的解释器将会消耗完系统中所有可用的内存,造成系统崩溃。

JavaScript的解释器可以检测到何时程序不再需要这个对象,可以把它所占用的内存释放掉了。

var a="hello world";
var b="world";
var a=b;	//会释放掉"hello world"

垃圾回收的方法

标记清除

  1. 获取根,标记。
  2. 访问标记对象并标记他们的引用
  3. 以此类推,
  4. 除标记对象,剩下都删除

virtual dom

用JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中。

当状态变更的时候

  1. 重新构造一棵新的对象树。
  2. 新的树和旧的树进行比较,记录两棵树差异
  3. 把所记录的差异应用到所构建的真正的DOM树上,视图就更新了。tual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。

webpack用来干什么的

webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个bundle。

setTimeout,setInterval

setTimeout(超时调用)

JS有一个任务队列,按顺序执行。setTimeout(fn,1000),1000毫秒后将fn添加到任务队列(若队列为空,立即执行)。

var ID = setTimeout(function(){	//调用setTimeout()之后,该方法会返回一个数值ID,表示超时调用
	alert("run");
},1000)
clearTimeout(ID);	//取消超时调用

封装一个函数,参数是定时器的时间,.then执行回调函数。

function sleep(time){

return new Promise((resolve)=>setTimeout(resolve,time));

}

setInterval(间歇调用)

和setTimeout类似,只是要按照指定的时间间隔重复执行代码,直至间歇调用被取消或页面被卸载。

var ID = setInterval(function(){	
	alert("run");
},1000)
clearInterval(ID);	

Event Loop

"Event Loop是一个程序结构,用于等待和发送消息和事件。

  1. 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
  2. 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
  3. 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  4. 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

宏任务,微任务

macro-task(宏任务):包括整体代码script,setTimeout,setInterval
micro-task(微任务):Promise,process.nextTick

console.log('script start')											
const interval = setInterval(() => {		//interval
    console.log('setInterval')
}, 0)
setTimeout(() => {					//timeout1
    console.log('setTimeout 1')
    Promise.resolve()				//then2
        .then(() => console.log('promise 3'))
        .then(() => console.log('promise 4'))
        .then(() => {
            setTimeout(() => {		//timeout2
                console.log('setTimeout 2')
                Promise.resolve().then(() => console.log('promise 5'))	//then3
                    .then(() => console.log('promise 6'))
                    .then(() => clearInterval(interval))
            }, 0)
        })
}, 0)
Promise.resolve()			//then1
    .then(() => console.log('promise 1'))
    .then(() => console.log('promise 2'))
  1. script start,
宏任务 微任务
interval then1
timeout1

promise1, promise2

  1. setInterval
宏任务 微任务
timeout1
interval
  1. setTimeout 1
宏任务 微任务
interval then2
interval
timeout2

promise 3, promise 4

  1. setInterval , setInterval
宏任务 微任务
timeout2
  1. setTimeout 2
宏任务 微任务
then3

promise 5, promise 6

ES5、ES6

https://www.jb51.net/article/166579.htm

https://www.jdon.com/idea/js/es5-es6.html

https://www.cnblogs.com/sunshinezjb/p/9248533.html

你可能感兴趣的:(javascript,前端)