JavaScript小记(持续更新)

注意:(2020.01.14) 后续将都在我的学习笔记文档 - JavaScript小记中进行更新,该文章不再更新,请持续关注上述网页

学习js遇到的疑问和js基础都记录在这里,持续更新中。


2019.12.11 更新this、继承,补充了一些方法,删除对象,创建对象的方法

2020.01.14 增加ES6部分新特性,转自ES6核心,教你 玩转 ES6新特性

2020.02.16 更新作用域,let&const

2020.02.24 重新整理,更新小记顺序(大致按《JavaScript高级程序设计》顺序来)


JavaScript概述

1、 一个完整的JavaScript实现应该由下列三个不同的部分组成

1.核心(ECMAScript) ——提供核心语言功能

2.文档对象模型(DOM) ——提供访问和操作网页内容的方法和接口

3.浏览器对象模型(BOM) ——提供与浏览器交互的方法和接口

ECMAScript和JavaScript的关系是,前者是后者的规格,后者是前者的一种实现(另外的ECMAScript方言还有JScript和ActionScript)。日常场合,这两个词是可以互换的。

2、JavaScript语言特性

1、运行在客户端浏览器上;

2、脚本语言、解释性语言,不用预编译,直接解析执行代码;

3、是弱类型语言,较为灵活;

4、与操作系统无关,跨平台的语言;

5、基于对象,是一种基于对象的脚本语言,它不仅可以创建对象,也能使用现有的对象。

3、JavaScript的优缺点

看这里~

4、JS内置对象

Arguments(函数参数集合)

Array(数组)

Boolean(布尔对象)

Date(日期对象)

Error(异常对象)

Function(函数构造器)

Math(数学对象)

Number(数值对象)

Object(基础对象)

RegExp(正则表达式对象)

String(字符串对象)

DOM

1、jQuery对象和原生DOM对象互相转化

两者区别:js原生获取的dom是一个对象,jQuery对象就是一个数组对象,其实就是选择出来的元素的数组集合,所以说他们两者是不同的对象类型不等价。

原生DOM对象转jQuery对象:

var box = document.getElementById('box');
var $box = $(box);

jQuery对象转原生DOM对象:

var $box = $('#box');
var box1 = $box[0];       // 方法一
var box2 = $box.get(0);   // 方法二

2、原生javascript和jQuery操作DOM的对比总结

3、Node节点

这是一张优秀的思维导图:JavaScript Node节点类型

节点属性

1、nodeType:节点类型

  • Node.ELEMENT_NODE(1);(元素节点)
  • Node.ATTRIBUTE_NODE(2);(属性节点)
  • Node.TEXT_NODE(3);(文本节点)
  • Node.CDATA_SECTION_NODE(4);
  • Node.ENTITY_REFERENCE_NODE(5);
  • Node.ENTITY_NODE(6);
  • Node.PROCESSING_INSTRUCTION_NODE(7);
  • Node.COMMENT_NODE(8);(注释节点)
  • Node.DOCUMENT_NODE(9);(document节点)
  • Node.DOCUMENT_TYPE_NODE(10);
  • Node.DOCUMENT_FRAGMENT_NODE(11);
  • Node.NOTATION_NODE(12)

2、nodeName:节点名称。元素节点为标签名,文本节点为#text

3、nodeValue:节点值。元素节点为null,文本节点为文本内容

节点关系

1、childNodes:当前节点的子节点的节点列表

2、parentNode:当前节点的父节点

3、firstChild:父节点的第一节点。相当于childNodes[0]或childNodes.item[0]

4、lastChild:父节点的最后一个节点

5、nextSibling:当前节点的后一个节点

6、previousSibling:当前节点的前一个节点

7、hasChildNodes:节点包含一或多个子节点的情况下返回true

8、ownerDocument:指向表示整个文档的文档节点

节点操作

1、createElement():创建元素节点

2、createAttribute():创建属性节点

3、appendChild():向节点的子节点列表的结尾添加新的子节点

4、cloneNode():复制节点

5、insertBefore(newNode,target):在指定的子节点前插入新的子节点

6、replaceChild(newNode,target):用新节点替换一个子节点

7、removeChild():删除(并返回)当前节点的指定子节点

8、normalize():将空文本节点删除或将相邻的文本节点合并一个文本节点

9、getAttribute():返回指定属性值

10、setAttribute():把指定属性设置或修改为指定的值

11、querySelectorAll():返回文档中匹配指定css选择器的所有元素,返回NodeList对象(集合)

4、DOM节点的attribute和property有何区别

  • property只是一个JS对象的属性的修改
  • attribute是对html标签属性的修改

请看这篇文章:JS中attribute和property的区别

5、window.onload和DOMConentLoaded的区别

window.addEventListener('load', function() {
    // 页面的全部资源加载完才会执行,包括图片,视频
})
document.addEventListener('DOMContentLoaded', function() {
    // DOM渲染完即可执行,此时图片、视频还可能没有加载完
})

BOM

1、BOM属性对象方法

什么是BOM? BOM(Browser Object Model)浏览器对象模型,是Javascript的重要组成部分。它提供了一系列对象用于与浏览器窗口进行交互,这些对象通常统称为BOM。

有哪些常用的BOM属性呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GigKwQ8R-1583129190135)(…\picture\bom.png)]

(1) location对象

location.href-- 返回或设置当前文档的URL

location.search – 返回URL中的查询字符串部分。例如 http://www.dreamdu.com/dreamdu.php?id=5&name=dreamdu 返回包括(?)后面的内容?id=5&name=dreamdu

location.hash – 返回URL#后面的内容,如果没有#,返回空

location.host – 返回URL中的域名部分,例如www.dreamdu.com

location.hostname – 返回URL中的主域名部分,例如dreamdu.com

location.pathname – 返回URL的域名后的部分。例如 http://www.dreamdu.com/xhtml/ 返回/xhtml/

**location.port – 返回URL中的端口部分。**例如 http://www.dreamdu.com:8080/xhtml/ 返回8080

**location.protocol – 返回URL中的协议部分。**例如 http://www.dreamdu.com:8080/xhtml/ 返回(//)前面的内容

http:

location.assign – 设置当前文档的URL

location.replace(url) – 设置当前文档的URL,并且在history对象的地址列表中移除这个URL

location.reload() – 重载当前页面

(2) history对象

history.go() – 前进或后退指定的页面数 history.go(num);

history.back() – 后退一页

history.forward() – 前进一页

(3) Navigator对象

navigator.userAgent – 返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串)

navigator.cookieEnabled – 返回浏览器是否支持(启用)cookie

(4) screen对象

screen.width

screen.height

数据类型

1、数据类型

另外可以看看这篇文章:js中的值类型和引用类型的区别

基本数据类型(值类型):undefined、null、string、number、boolean、symbol

(占用空间固定,保存在栈中)

复杂数据类型:object

引用类型:array、function、object

(占用空间不固定,保存在堆中。存储在栈中的是指向堆中的数组或者对象的地址)

2、堆区和栈区

栈区的特点:操作性能高,速度快,存储量小

​ 所以:一般存储操作频率较高,生命周期较短,占用空间较小的数据。(基本数据类型)

堆区的特点:操作性能低(相对于栈区),速度慢,存储量大

​ 所以:一般存储操作频率较低,生命周期较长,占用空间较大的数据。(复杂数据类型)

​ 堆通常是一个可以被看做一棵完全二叉树的数组对象。 逻辑上:完全二叉树 存储上:数组(顺序存储)

3、JS中数据类型的判断( typeof,instanceof,constructor,Object.prototype.toString.call() )

typeof

对一个值使用typeof操作符可能返回:

undefined、string、number、boolean、object(对象或null)、function、symbol(新)

console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof []);              // object  []数组的数据类型在 typeof 中被解释为object
console.log(typeof function(){});    // function
console.log(typeof {});              // object
console.log(typeof undefined);       // undefined
console.log(typeof null);            // object    null 的数据类型被 typeof 解释为 object

typeof 对于基本类型,除了null都可以显示正确的类型;对于对象,除了函数都会显示object。

对于null来说,虽然它是基本类型,但是会显示object,这是一个存在了很久的bug。

因为在js的最初版本中,使用的是32位系统,为了性能考虑使用低位存储了变量的类型信息,000开头代表是对象,然而null表示为全零,所以将它错误的判断为object。虽然现在的内部类型 判断代码已经改变了,但是对于这个bug却是一直流传下来。

instanceof

只有引用数据类型(Array,Function,Object)被精准判断,其他(数值Number,布尔值Boolean,字符串String)字面值不能被instanceof精准判断。

用于判断引用类型属于哪个构造函数的方法。

instanceof可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找得类型的prototype

f instanceof Foo 判断逻辑 f 的_proto_一层层往上,能否对应到Foo.prototype

console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false  
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true    
// console.log(undefined instanceof Undefined);
// console.log(null instanceof Null);
function Person(name, age) {
    this.name = name;
    this.age = age;
}
function Dog(name, age) {
    this.name = name;
    this.age = age;
}
var p = new Person('zs', 18);
var d = new Dog('小花', 8);

console.log(p instanceof Person);       // true
console.log(d instanceof Person);       // true
console.log(p instanceof Object);		// false
constructor
console.log((2).constructor === Number);  				// true
console.log((true).constructor === Boolean);  			// true
console.log(('str').constructor === String); 			// true
console.log(([]).constructor === Array);  				// true
console.log((function() {}).constructor === Function);  // true
console.log(({}).constructor === Object);               // true

用costructor来判断类型看起来是完美的,然而,如果我创建一个对象,更改它的原型,这种方式也变得不可靠了。

function Person(name, age) {
    this.name = name;
    this.age = age;
}

var p = new Person('csm', 21);

console.log(p.constructor.name); 	// Person

// 改变原型
Person.prototype = {
    name: 'zs',
    age: 18
};

var p1 = new Person('csm', 21);

console.log(p1.constructor.name); 	// Object

因此,当要修改对象的proptotype时,一定要设置constructor指向其构造函数

function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype = {
	constructor: Person,
    name: 'zs',
    age: 18
};
var p = new Person('csm', 21);
console.log(p.constructor.name); 	// Person
object.prototype.toString.call()
console.log(Object.prototype.toString.call(2));    			// [object Number]
console.log(Object.prototype.toString.call(true));			// [object Boolean]
console.log(Object.prototype.toString.call('str'));			// [object String]
console.log(Object.prototype.toString.call([]));			// [object Array]
console.log(Object.prototype.toString.call(function(){}));	// [object Function]
console.log(Object.prototype.toString.call({}));			// [object Object]
console.log(Object.prototype.toString.call(undefined));		// [object Undefined]
console.log(Object.prototype.toString.call(null));			// [object Null]

​ 使用 Object 对象的原型方法 toString ,使用 call 进行狸猫换太子,借用Object的 toString 方法结果精准的显示我们需要的数据类型。就算我们改变对象的原型,依然会显示正确的数据类型。

4、object和Object

JavaScript中object和Object有什么区别,为什么用typeof检测对象,返回object,而用instanceof 必须要接Object呢?

typeofinstanceof 这两个功能就是完全不一样的运算符。typeof 是为了检查数据类型,instanceof是为了看一个变量是否是某个对象的实例。

typeof 返回的结果,是一个字符串。所以object就是一个字符串。

Object 是 JavaScript 中一个重要的内置对象,其它对象都是基于它的,包括你创建的函数。

5、function和Function

ECMAScript 的Function实际上就是一个功能完整的对象。

而function是用来创建所有对象的构造函数或者普通函数要用的关键字

var a = new function(){}实际上是用构造函数的方法创建了一个匿名对象的实例,而并不是系统内置对象Function的实例,所以a instanceof Function返回falsetypeof返回"object"

6、null和undefined的区别

  • Undefined类型只有一个值,即undefined。当声明的变量还未被初始化时,变量的默认值为undefined。
    • 变量被声明了,但没有赋值时,就等于undefined。
    • 调用函数时,应该提供的参数没有提供,该参数等于undefined。
    • 对象没有赋值的属性,该属性的值为undefined。
    • 函数没有返回值时,默认返回undefined。
  • Null类型也只有一个值,即null。null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。
    • 作为函数的参数,表示该函数的参数不是对象。
    • 作为对象原型链的终点。

操作符

1、&&和||

短路原理

1、只要“&&”前面是false,无论“&&”后面是true还是false,结果都将返“&&”前面的值;

2、只要“&&”前面是true,无论“&&”后面是true还是false,结果都将返“&&”后面的值;

1、只要“||”前面为false,不管“||”后面是true还是false,都返回“||”后面的值。

2、只要“||”前面为true,不管“||”后面是true还是false,都返回“||”前面的值。

2、只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。

3、NaN

var result = “a” < 3; // false,因为“a”被转化成了NaN

根据规则,任何操作数与NaN进行关系比较,结果都是false

4、{} == false / {} == {}

"=="运算符比较"喜欢"Number类型

{} == false  <=> Number({}) == Number(false) <=> NaN == 0  // false
{} == {} // false  两者指向的不是同一个地址

String

1、String对象中slice()、substring()、substr()的用法与区别

  • slice() 方法可提取字符串的某个部分,并以新的字符串返回被提取的部分。
    • 语法:stringObject.slice(start,end)
    • 返回一个新的字符串。包括字符串 stringObject 从 start 开始(包括 start)到 end 结束(不包括 end)为止的所有字符。
    • 如果是负数,则该参数规定的是从字符串的尾部开始算起的位置。也就是说,-1 指字符串的最后一个字符,-2 指倒数第二个字符,以此类推。
  • substring() 方法用于提取字符串中介于两个指定下标之间的字符。
    • 语法:stringObject.substring(start,stop)
    • 一个新的字符串,该字符串值包含 stringObject 的一个子字符串,其内容是从 start 处到 stop-1 处的所有字符,其长度为 stop减 start。
    • 如果 start 比 stop 大,那么该方法在提取子串之前会先交换这两个参数。
    • 与 slice() 和 substr() 方法不同的是,substring() 不接受负的参数。
  • substr() 方法可在字符串中抽取从 start 下标开始的指定数目的字符。
    • 语法:stringObject.substr(start,length)
    • 一个新的字符串,包含从 stringObject 的 start(包括 start 所指的字符) 处开始的 length 个字符。如果没有指定 length,那么返回的字符串包含从 start 到 stringObject 的结尾的字符。
    • 如果是负数,那么该参数声明从字符串的尾部开始算起的位置。也就是说,-1 指字符串中最后一个字符,-2 指倒数第二个字符,以此类推。
    • ECMAscript 没有对该方法进行标准化,因此反对使用它。

String 对象的方法 slice()、substring() 和 substr() (不建议使用)都可返回字符串的指定部分。slice() 比 substring() 要灵活一些,因为它允许使用负数作为参数。slice() 与 substr() 有所不同,因为它用两个字符的位置来指定子串,而 substr() 则用字符位置和长度来指定子串。

还要注意的是,String.slice() 与 Array.slice() 相似。

2、ES6中字符串的扩展

转自ES6核心,教你 玩转 ES6新特性

1.includes(),startsWith(),endsWith()

在ES6之前,js中只有indexof方法,来确定一个字符串中是否包含在另一个字符串中。

ES6中又提供了三种新的方法。

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let str = 'hello My word!'
console.log(str.startsWith("hello"));//true
console.log(str.endsWith('!'));//true
console.log(str.includes('My'));//true
2.padStart(),padEnd()

ES6引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。

padStart()用于头部补全,padEnd()用于尾部补全。

console.log('foo'.padStart(4,'a'));//afoo
console.log('foo'.padEnd(4,'a'));//fooa

console.log('foo'.padStart(3,'a'));//foo
console.log('foo'.padEnd(3,'a'));//foo

padstart最常见的用途

1.是用来补全在指定的位数。

2.是用来提示字符串格式:

'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

Array

1、Array对象中slice() 、splice()的区别

slice() 方法可从已有的数组中返回选定的元素。

  • 语法:arrayObject.slice(start,end)
  • 返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。在只有一个参数的情况下,返回从该参数指定位置开始到当前数组末尾的所有项。
  • 该方法不改变原字符串,而是返回新的字符串。如果想删除数组中的一段元素,应该使用方法 Array.splice()。
  • 如果slice()方法的参数中有一个负数,则用数组长度加上该数来确定相应的位置。
  • 如果结束为止小于起始位置,则返回空数组。

splice() 方法主要用途是向数组的中部插入项。

  • 语法:arrayObject.splice(index,howmany,item1,.....,itemX)

  • index必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。
    howmany必需。要删除的项目数量。如果设置为 0,则不会删除项目。
    item1, …, itemX可选。向数组添加的新项目。
  • splice()方法始终都会返回一个数组,该数组中包含从原始数组中删除的项(如果没有删除任何项,则返回一个空数组)

2、Map和ForEach的区别

  • forEach(): 数组中的每个元素执行一次回调函数
  • map(): 返回一个由原数组中的每个元素调用一个指定方法后的返回值组成的新数组

示例

下方提供了一个数组,如果我们想将其中的每一个元素翻倍,我们可以使用mapforEach来达到目的。

let arr = [1, 2, 3, 4, 5];

ForEach

注意,forEach是不会返回有意义的值的。另外,forEach不支持break和continue(every也不可以)
我们在回调函数中直接修改arr的值。

arr.forEach((num, index) => {
    return arr[index] = num * 2;
});

Map

let doubled = arr.map(num => {
    return num * 2;
});

3、indexOf与search的区别

  • indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置,如果没有找到返回-1
    • 该方法将从头到尾地检索字符串stringObject,看它是否含有子串searchvalue。开始检索的位置在字符串的fromindex处。如果没有fromindex参数则从字符串的开头检索。如果找到一个searchvalue,则返回searchvalue的第一次出现的位置。stringObjec中的字符串位置是从0开始的。
    • indexOf()方法对大小写敏感。
  • search方法用于检索字符串中指定的子字符串,检索与正则表达式相匹配的子字符串。如果没有找到,返回-1。
    • search() 方法不执行全局匹配,它将忽略标志 g。它同时忽略 regexp 的 lastIndex 属性,并且总是从字符串的开始进行检索,这意味着它总是返回 stringObject 的第一个匹配的位置。
    • search() 方法对大小写敏感。

indexOf与search的区别

search()的参数必须是正则表达式,而indexOf()的参数只是普通的字符串。indexOf()是比search()更加底层的方法。

如果只是对一个具体字符串来检索,那么使用indexOf()的系统资源消耗更小,效率更高;如果查找具有某些特征的字符串(例如查找以a开头,后面是数字的字符串),那么indexOf()就无能为力,必须要使用正则表达式和search()方法了。

大多是时候用indexOf()不是为了真的想知道子字符串的位置,而是想知道长字符串中有没有包含这个子字符串。r如果返回索引为-1,那么说明没有,反之则有。

4、查找数组

let array = [1, 2, 3, 4, 5]
let find = array.find(function(item) {
    return item === 3
})
console.log(find) // 3 (返回的是值,只输出符合的第一个值,没找到则返回undefined)

返回找到值的位置:findIndex

5、Array.of

转自ES6核心,教你 玩转 ES6新特性

Array.of主要是用于将一组值转化为数组,创建一个可变数量参数的新数组,而不考虑参数的类型和数量

Array.of的出现主要是对Array创建数组的不足进行的补充,因为用Array创建数组,因为参数个数的不同,会造成差异。 Array.of()和Array构造函数的区别:在于处理整数参数

如下:

Array()//[]
Array(3)//[,,,]
Array(1,2,3)//[1,2,3]
//当Array()中的参数只有一个的时候,此时的参数就代表了数组的长度,只有参数大于等于2的时候才会返回,参数组成的新数组。
//Array.of
console.log(Array.of())//[]
console.log(Array.of(3))//[3]
console.log(Array.of(3).length)//1
//而Array无论参数有几个最后都会返参数组成的数组。

Array.of功能的模拟实现如下

function arrayof{
    return Array.slice.call(arguments);
}

6、Array.from()

Array.from()方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组。

Array.from(arrayLike, mapFn, thisArg)

参数

  • arrayLike

    想要转换成数组的伪数组对象或可迭代对象。

  • mapFn 可选

    如果指定了该参数,新数组中的每个元素会执行该回调函数。

  • thisArg 可选

    可选参数,执行回调函数 mapFnthis 对象。

返回值:一个新的数组实例。

例1:Array.from ({length:n}, Fn) 将类数组转换为数组

Array.from({length:3}, () => 'jack') //["jack", "jack", "jack"]
let array = {
    0: 'name', 
    1: 'age',
    2: 'sex',
    3: ['user1','user2','user3'],
    'length': 4
}
let arr = Array.from(array)
console.log(arr) // ['name','age','sex',['user1','user2','user3']]

如果将上面代码中length属性去掉,arr将会是一个长度为0的空数组[]

例2:Array.from (obj, mapFn)

obj指的是数组对象、类似数组对象或者是set对象,map指的是对数组中的元素进行处理的方法。

let set = new Set([1,1,2,5,5,6,7,8,9])
console.log(Array.from(set))  // [1, 2, 5, 6, 7, 8, 9]
//将数组中布尔值为false的成员指为0
Array.from([1, ,2,3,3], x => x || 0) //[1,0,2,3,3]
 
//将一个类似数组的对象转为一个数组,并在原来的基础上乘以2倍
let arrayLike = { '0': '2', '1': '4', '2': '5', length: 3 }
Array.from(arrayLike, x => x*2) //[4,8,10]
 
//将一个set对象转为数组,并在原来的基础上乘以2倍
Array.from(new Set([1,2,3,4]), x => x*2) //[2,4,6,8]

例3:Array.from(string) 将字符串转换为数组

Array.from('abc') //['a','b','c']

例子4:将Map解构转为数组,最方便的做法就是使用扩展运算符(…)

const myMap = new Map().set(true, 7)
console.log(myMap); //Map(1) {true => 7}
console.log([...myMap]); //[true ,7]

7、伪数组转化为数组

伪数组

1、以索引方式存储数据

2、有一个length

方法一、 声明一个空数组,通过遍历伪数组把它们重新添加到新的数组中

var aLi = document.querySelectorAll('li');
var arr = [];
for (var i = 0; i < aLi.length; i++) {
	arr[arr.length] = aLi[i]
}

方法二、使用数组的slice()方法 它返回的是数组,使用call或者apply指向伪数组

var arr = Array.prototype.slice.call(arg);  // 方法一
var arr = [].slice.call(arg);   			// 方法二

方法三、使用原型继承

oldarr.__proto__ = Array.prototype;

方法四、ES6的数组新方法

Array.from({length:3}, function() { return 1}) // [1,1,1] 初始化并赋值 

8、清空数组的方法

方式1:splice函数

var arr = [1,2,3,4];  
arr.splice(0);

方式2:给数组的length赋值为0

var arr = [1,2,3,4];  
arr.length = 0;

赋予数组的长度小于本身的长度,数组中后面的元素将被截断。

赋予数组的长度大于本身的长度,将扩展数组长度,多的元素为undefined。

方式3:直接赋予新数组 []

<script type ="text/javascript">  
  var arr = [1,2,3,4];  
   arr = [];
</script> 

这种方式为将arr重新复制为空数组,之前的数组如果没有被引用,将等待垃圾回收。

9、数组去重的方法

1、利用ES6 Set去重(ES6中最常用)

function unique (arr) {
  return Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
 //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

不考虑兼容性,这种去重的方法代码最少。这种方法无法去掉“{}”空对象。

2、利用for嵌套for,然后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]){         
                arr.splice(j,1);
                j--;
            }
        }
    }
	return arr;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
    //[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}]     
    //NaN和{}没有去重,两个null直接消失了

3、for…of + includes()

双重for循环的升级版,外层用 for…of 语句替换 for 循环,把内层循环改为 includes()

先创建一个空数组,当 includes() 返回 false 的时候,就将该元素 push 到空数组中

类似的,还可以用 indexOf() 来替代 includes()

function unique(arr) {
    var result = []
    for (var i of arr) {
        if (!result.includes(i)) {
            result.push(i)
        }
    }
    return result
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
   // [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]      //NaN、{}没有去重

4、利用indexOf去重

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!');
        return;
    }
    var array = [];
    for (var i = 0; i < arr.length; i++) {
        if (array.indexOf(arr[i]) === -1) {
            array.push(arr[i]);
        }
    }
    return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
   // [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]      //NaN、{}没有去重

5、利用hasOwnProperty

function unique(array) {
    var obj = {};
    var unique = [];
    for(var i = 0; i < array.length; i++) {
        if(!obj.hasOwnProperty([array[i]])) {
            obj[array[i]] = 1;
            unique.push(array[i]);
        }
    }
    return unique;
}

    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}]   //所有的都去重了

6、利用filter

function unique(arr) {
  return arr.filter(function(item, index, arr) {
    //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
    return arr.indexOf(item, 0) === index;
  });
}
    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
        console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {…}, {…}]

7、利用Map数据结构去重

function arrayNonRepeatfy(arr) {
  let map = new Map();
  let array = new Array();  // 数组用于返回结果
  for (let i = 0; i < arr.length; i++) {
    if(map.has(arr[i])) {  // 如果有该key值
      map.set(arr[i], true); 
    } else { 
      map.set(arr[i], false);   // 如果没有该key值
      array.push(arr[i]);
    }
  } 
  return array;
}
 var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
 console.log(arrayNonRepeatfy(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]
// {}没去

创建一个空Map数据结构,遍历需要去重的数组,把数组的每一个元素作为key存到Map中。由于Map中不会出现相同的key值,所以最终得到的就是去重后的结果。

8、利用reduce+includes

function unique(arr){
    return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]

9、for…of + Object

首先创建一个空对象,然后用 for 循环遍历

利用对象的属性不会重复这一特性,校验数组元素是否重复

function unique(arr){
    let result = []
    let obj = {}
    for (let i of arr) {
        if (!obj[i]) {
            result.push(i)
            obj[i] = 1
        }
    }
    return result
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr));
// [1, "true", 15, false, undefined, null, NaN, 0, "a", {…}]

10、数组拍平

1、递归

function flatArr(arr) {
	var newArr = [];
	arr.forEach((val) => {
		if (Array.isArray(val)) {
			newArr = newArr.concat(flatArr(val))
		} else {
			newArr.push(val)
		}
	})
	return newArr;
}

2、reduce

function flatArr(arr) {
   return arr.reduce((pre, cur) => pre.concat(Array.isArray(cur) ? flatArr(cur) : cur), [])
}

3.利用数组join()或toString()方法

// join()
function flatArr(arr) {
	return JSON.parse(`[${arr.join()}]`)
}
// toString()
function flatArr(arr) {
	return JSON.parse(`[${arr.toString()}]`)
}

4.es6数组的flat()方法 (浏览器版本过低不支持)

flat方法默认打平一层嵌套,也可以接受一个参数表示打平的层数,传 Infinity 可以打平任意层。

function flatArr(arr) {
  	return arr.flat(Infinity);
}

11、随机打乱数组

function randomsort(a, b) {
    return Math.random() > .5 ? -1 : 1;
    //用Math.random()函数生成0~1之间的随机数与0.5比较,返回-1或1
}
var arr = [1, 2, 3, 4, 5];
arr.sort(randomsort);

// es6 写法
arr.sort(() => Math.random() - 0.5);

12、根据数组中对象的某一个属性值进行排序

var arr = [
    {name:'zopp',age:0},
    {name:'gpp',age:18},
    {name:'yjj',age:8}
];

// 方法一
function compare(property){
    return function(a,b){
        var value1 = a[property];
        var value2 = b[property];
        return value1 - value2;
    }
}
console.log(arr.sort(compare('age')));

// 方法二
console.log(arr.sort((a,b) => a.age - b.age));

Object

1、深拷贝和浅拷贝

区别

1.浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用

2.深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“”(数组的所有元素)拷贝过来,是“值”而不是“引用”

为什么要使用深拷贝?

我们希望在改变新的数组(对象)的时候,不改变原数组(对象)

深拷贝数组

1.直接遍历

function copyArr(arr) {
    let res = []
    for (let i = 0; i < arr.length; i++) {
     res.push(arr[i])
    }
    return res
}
var arr = [1,2,3,4,5]
var arr2 = copyArr(arr)

2.slice()

var arr = [1,2,3,4,5]
var arr2 = arr.slice()

3.concat()

var arr = [1,2,3,4,5]
var arr2 = arr.concat()

深拷贝对象

迭代递归法

这是最常规的方法,思想很简单:就是对对象进行迭代操作,对它的每个值进行递归深拷贝。

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;
}
function deepClone(obj, newObj) {
  for (var key in obj) {
    var val = obj[key];
    if (val instanceof Object) {   
      var tempObj = new val.constructor;
      deepClone(val, tempObj);
      newObj[key] = tempObj;
    } else {
      newObj[key] = val;
    }
  }
}

“一招鲜,吃遍天” ——序列化反序列化法**

JSON.parse(JSON.stringify(XXXX))

var array = [
    { number: 1 },
    { number: 2 },
    { number: 3 }
];
var copyArray = JSON.parse(JSON.stringify(array))
copyArray[0].number = 100;
console.log(array); //  [{number: 1}, { number: 2 }, { number: 3 }]
console.log(copyArray); // [{number: 100}, { number: 2 }, { number: 3 }]

它也只能深拷贝对象和数组,对于其他种类的对象,会失真。这种方法比较适合平常开发中使用,因为通常不需要考虑对象和数组之外的类型。

缺点:

  • 会忽略undefined
  • 不能序列化函数
  • 不能解决循环引用的对象

在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。当然如果你的数据中含有以上三种情况下,可以使用lodash的深拷贝函数。

2、Object.assign()

语法:Obejct.assign(target,...sources)

用途:将来自一个或多个源对象中的值复制到一个目标对象,它将返回目标对象。

其中对象的继承属性和不可枚举属性是不能拷贝的,所以不能使用它来实现深拷贝。

assign这个方法会把原型上面的内容也拷贝了。

let obj = { name:"goudan"}
let obj2 = { name:"xiaofei",age:12}
obj2.__proto__.sex = "man"
console.log(Object.assign(obj,obj2));//在obj的__proto__对象上面也会有sex属性

第一级属性深拷贝,从第二级属性开始就是浅拷贝。

如果多个源对象具有同名属性,则排位靠后的源对象会覆盖排位靠前的。但null或undefined被视为空对象一样对待,不会覆盖。

var receiver = {};
Object.assign(receiver, 
    {
    	type: "js", 
    	name:"file.js"
	},
    {
    	type: "css"
	}
)
console.log(receiver); // {type: "css", name: "file.js"}
//实现 deepAssign({a: {b: 1, c: 2}}, {a: {c: 3}});
//=> {a: {b: 1, c: 3}}

// 只有两个对象
function deepAssign(obj1, obj2){
    for(var item in obj2){
        obj1[item] = typeof obj2[item] === 'object' ? deepClone(obj1[item], obj2[item]) : obj2[item];
    }
    return obj1;
}
// 通用
function deepAssign() {
  var args = Array.from(arguments);
  return args.reduce(deepClone, args[0]);

  function deepClone(target, obj){
    if(!target) target = Array.isArray(obj) ? [] : {};
      for(key in obj){
        target[key] = typeof obj[key] ==="object" ? deepClone(target[key], obj[key]) : obj[key]
      }
    return target;
  }
}

RexgExp等

1、正则表达式

元字符
元字符作用
.匹配任意字符除了换行符和回车。
[]匹配方括号内的任意字符。比如[0-9]就可以用来匹配任意数字。
^^9,这样使用代表匹配以9开头。[ ^9 ],这样使用代表不匹配方括号内除了9的字符。
{n,m}匹配n~m位字符。如 /b{n,m}/g,就是最少出现n次b,最多出现m次b。
(abc)只匹配和abc相同的字符串。
|匹配|前后任意字符。
\转义。
*匹配0~n位字符。如 /b*/g,就是可以不出现b,也可以出现一次或多次。
+匹配1~n位字符。如 /b+/g,就是至少出现一次b。
??之前字符可选,0或1位。如 /colou?r/g,就是可以匹配color或colour。
修饰语
修饰语作用
i忽略大小写
g全局搜索
m多行
字符简写
简写作用
\w匹配字母数字或下划线
\W和上面相反
\s匹配任意的空白符
\S和上面相反
\d匹配数字
\D和上面相反
\b匹配单词的开始和结束
\B和上面相反
检索规则

1.test()方法用来检测一个字符串是否匹配某个正则表达式,如果匹配成功,返回true,否则返回false

2.exec()方法用来检索字符串中与正则表达式匹配的值。exec()方法返回一个数组,其中存放匹配的结果。如果未找到匹配的值,则返回null

3.compile()方法可以在脚本执行过程中编译正则表达式,也可以改变已有表达式

创建方式

1、字面量语法:/pattern/ attributes

2、创建RegExp对象的语法:new RegExp(pattern, attributes)

attributes:g:全局匹配 i:大小写匹配 m:多行匹配

例一:删除一个字符串中所有的英文

doc = doc.replace(/[A-Za-z]+/g, '');

例二:验证一个字符串是否是电话号码

/^[1][358][0-9]{9}$/

例三:将原字符串中的所有空白字符替换成""

replace(/\s/g,"")

"/ /“这个是固定写法,”\s"是转移符号用以匹配任何空白字符,包括空格、制表符、换页符等等,"g"表示全局匹配将替换所有匹配的子串,如果不加"g"当匹配到第一个后就结束了。

2、正则表达式的先行断言(lookahead)和后行断言(lookbehind)

看这里~

Function 函数

1、形参和arguments

该参数是一个类似于数组的结构(可以像数组一样遍历,还可以使用下标来访问数据),但是并不是数组。

1、函数调用的时候,会把实参的值赋给形参,而且会使用arguments来接收实参

2、如果实参的个数超过形参的个数,那么可以通过arguments来获取超出的数据

3、如果实参的个数小于形参的个数,那么不足的全部设置为undefined

两者之间是关联的关系

函数名.length => 形参的长度(个数)

2、在函数体内可以通过arguments对象来访问这个参数数组,从而获取传递给函数的每一个参数

function doadd() {
    if (arguments.length == 1) {
        alert(arguments[0] + 10);
    } else if (arguments.length == 2) {
        alert(arguments[0] + arguments[1]);
    }
}
doAdd(10) // 20
doAdd(30, 20) // 50

3、立即执行函数

声明一个函数,并马上调用这个匿名函数就叫做立即执行函数;也可以说立即执行函数是一种语法,让你的函数在定义以后立即执行。

立即执行函数的作用:

  1. 不必为函数命名,避免了污染全局变量

  2. 立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量

    例子:

var foo = 1;
(function foo() {
     foo = 10; 
     console.log(foo);
}())
// ƒ foo() { foo = 10; console.log(foo) }

​ 因为当js解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到foo,所以打印的还是这个函数,并且外部的值也没有发生更改。

作用域

1、变量提升

变量定义

console.log(a); // undefined
var a = 100;

实际 => 变量定义提升(PS:变量声明的提升仅仅是声明提升了,赋值并不会提升)
var a;
console.log(a)
a = 100;

函数声明

fn('zhangsan');  // zhangsan 20
function fn(name) {
    age = 20;
    console.log(name, age);
    var age;
}

注意:函数声明和函数表达式的区别

函数声明: function fn() {} fn()不会报错,函数声明能提前。

函数表达式:var fn1 = function() {} fn1()报错,这时只有变量声明提前。

另外:函数声明的提升在变量声明的提升之上的

2、this

另外,可以看看这篇文章:https://www.cnblogs.com/pengshengguang/p/11105323.html

this对象是在运行时基于函数的执行环境绑定的:

  • 在全局函数中,this—>window

  • 在函数中

    1、作为对象的方法来调用 this—>当前的对象

    var obj = {
        name: 'A',
        printName: function() {
            console.log(this.name)
            console.log(this) // this -> obj这个被新创建出来的对象
        }
    }
    obj.printName()
    

    2、作为普通的函数调用 this—>window

    (1.2可总结为 看函数名前面是否有“.” ,有的话, “.”前面是谁,this就是谁;没有的话this就是window )

    function fn() {
        console.log(this) // this -> window
    }
    fn()
    

    3、作为构造函数和new使用 this—>构造函数内部新创建的对象

    function Foo(name) {
        this.name = name
        console.log(this)  // this -> f这个被新创建出来的对象
    }
    var f = new Foo('zhangsan')
    

    4、被call,apply或bind调用(函数上下文调用) this—>第一个参数

    function fn1(name, age) {
        console.log(name)
        console.log(this) // {x: 100}
    }
    fn1.call({x: 100}, 'zhangsan', 20)
    
    function fn2(name, age) {
        console.log(name)
        console.log(this) // {y: 200}
    }
    fn2.bind({y: 200}, 'zhangsan', 20)()
    

    5、立即执行函数中的this永远都是window

    (function() {
        console.log(this); // this->window
    })()
    

    6、箭头函数不绑定this,会捕获其所在的上下文的this值,作为自己的this值。

    箭头函数本身没有this,箭头函数中的this是在它声明时捕获它所处作用域中的this。

    特别说明:this一旦被捕获,以后将不再变化。

    var a = 'aaa'
    let func = () => {
        console.log(this.a)
        console.log(this);  // this -> window
    }
    func();
    // 箭头函数在全局作用域声明,所以它捕获全局作用域中的this,this指向window对象。
    
    var a = 'aaa';
    function wrap(){
      console.log(this)  // this -> owrap这个被新创建出来的对象
      this.a = 'bbb';
      let func = () => {
        console.log(this.webName);
        console.log(this);  // this -> owrap这个被新创建出来的对象
      }
      func();
    }
    let owrap = new wrap();
    // 函数作用域中的this指向创建的对象实例。箭头函数也随之被声明,此时捕获这个this,也指向创建的对象实例
    

    通过 call() 或 apply() 方法调用一个函数时,只传入了一个参数,对 this 并没有影响

    var a = 'aaa';
    let b = {a: 'bbb'}; 
    
    function fn1() { 
      console.log(this);   // {a: 'bbb'}
      console.log(this.a); // bbb
    } 
    fn1.call(b);
    
    let fn2 = () => { 
      console.log(this);   	// window
      console.log(this.a);  // aaa
    } 
    fn2.call(b);
    

3、apply,call与bind

共同点:

1、都是用来改变函数的this对象的指向的。
2、第一个参数都是this要指向的对象。
3、都可以利用后续参数传参。

区别:

  • apply:最多只能有两个参数——新this对象和一个数组arrArray立即调用

    xw.say.apply(xh, [1,2,3,4]);

  • call:它可以接受多个参数,第一个参数与apply一样,后面则是一串参数列表立即调用

    xw.say.call(xh, 1, 2, 3, 4);

  • bind:bind() 方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind() 方法的第一个参数 作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。bind改变this作用域会返回一个新的函数,这个函数不会马上执行

    xw.say.bind(xh, 1, 2, 3, 4)();xw.say.bind(xh)(1, 2, 3, 4);

4、作用域

作用域是指对某一变量和方法具有访问权限的代码空间。

  • 全局作用域:在函数或代码块{}外定义的变量拥有全局作用域,即对任何内部函数来说,都是可以访问的。

    注意:在函数内部或代码块中没有定义的变量实际上是作为window/global的属性存在,而不是全局变量。换句话说没有使用var定义的变量虽然有用全局作用域,但是它是可以被delete,而全局变量不可以。

  • 块级作用域:存在于函数内部或块中(字符{和}之间的区域)

5、作用域链

var a = 100
function F1() {
    var b = 200
    function F2() {
        var c = 300
        console.log(a)	// a是自由变量,向父级作用域寻找a,未果,再向上一级父级作用域寻找a
        console.log(a)	// b是自由变量,向父级作用域寻找b
        console.log(a)
    }
    F2()
}
F1()               // 自由变量不断地往父级作用域去找,形成一个链式结构

改变作用域链的方法

with、try…catch、eval

例题:

var a = 100
function F1() {
    var a = 200
    F2()
}
function F2() {
    console.log(a)
}
F1()             // 100
var a = 100
function F1() {
    var a = 200
    function F2() {
        console.log(a)
    }
    F2()
}
F1()             // 200

闭包

1、闭包以及实际应用

**闭包就是能够读取其他函数内部变量的函数。**只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。

简单来说:闭包是在 A 函数里面返回的 B 函数,然后 B 函数里面一直引用着 A 函数的布局变量。

闭包的用途

1、读取函数内部的变量,并且让这些变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)。

2、让函数外部可以操作(读写)函数内部的数据(变量/函数)

3、封装变量,收敛权限

缺点:

1、容易形成循环引用,比普通函数占用更多的内存。

2、闭包会导致原始作用域链不释放,造成内存泄漏。

3、参数和变量不会被垃圾回收机制回收。

解决方法:

  • 把造成循环引用的变量设为null
  • 不用的变量及时释放
    function fn1() {
        var arr = new Array[99999999999999999];
        function fn2() {
            console.log(arr);
        }
        return fn2;
    }

    var f = fn1();
    f();

    f = null;

闭包的实际应用

1.通过闭包可以让内层函数延迟执行,只有需要时再执行它

function highLevelFunct2() {
    var arr = ['a', 'b', 'c'];
    var size = function () {
        alert(arr.length);
    };
    return size;
}


function testClosure() {
    var f = highLevelFunct2();
    f();
}

2.通过闭包,内层函数可以将外层函数的局部变量进行封装,封闭后就只有内层函数才能访问到外层函数的局部变量。

//  计数器
function myCounter() {
    var counter = 0;
    return function add () {
        return counter += 1;
    }
}

function testClosure() {
    var increase = myCounter();
    increase();	// 1
    increase(); // 2
    increase(); // 3
}

3.定义JS模块

  • 将所有的数据和功能都封装在一个函数内部(私有的)
  • 只向外暴露一个包含多个方法的对象或函数
  • 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
// MyTool.js
function myTool() {
    // 1. 私有的数据
    var money = 1000;
    // 2. 提供操作私有数据的函数
    function get() {
        money *= 10;
        console.log('赚了一笔钱, 总资产:' + money + '元');
    }

    function send() {
        money --;
        console.log('花了一笔钱, 总资产:' + money + '元');
    }

    return {
        'get': get,
        'send': send
    }
}

<script type="text/javascript" src="js/MyTool.js"></script>
<script type="text/javascript">
   var toolObj = myTool();
   toolObj.get();
   toolObj.send();
</script>
// MyTool1.js
(function (w) {
    // 1. 私有的数据
    var money = 1000;
    // 2. 提供操作私有数据的函数
    function get() {
        money *= 10;
        console.log('赚了一笔钱, 总资产:' + money + '元');
    }

    function send() {
        money --;
        console.log('花了一笔钱, 总资产:' + money + '元');
    }

    // 向外部暴露对象
    w.myTool = {
        'get': get,
        'send': send
    }

})(window);

<script src="js/MyTool1.js"></script>
<script>
    (function () {
        console.log(window.myTool, myTool);
        myTool.get();
        myTool.send();

        var str = 'zhangsan';
        console.log(str);
        console.log(window.str);
    })();
</script>

4.实现函数节流

   function throttle(fn, delay) {
        var timer = null;
        return function () {
            clearTimeout(timer);
            timer = setTimeout(fn, delay);
        }
    }
   throttle(function () {
       console.log('大家好!!!');
   }, 200)();

内存泄漏

1、代码回收规则

1.全局变量不会被回收

2.局部变量会被回收,也就是函数一旦运行完以后,函数内部的东西都会被销毁

3.只要被另外一个作用域所引用就不会被回收(闭包)

2、内存溢出和内存泄漏

内存溢出

一种程序运行出现的错误

当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误

var arrObj = {};
for(var i=0; i<100000000; i++){
	arrObj[i] = new Array(999999);
	console.log(arrObj);
}
内存泄漏

内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。

  • 占用的内存没有及时释放

  • 内存泄漏积累多了就容易导致内存溢出

    常见的内存泄漏:

  1. 占用内存很大的全局变量
  2. 没有及时清理的计时器/定时器
  3. 闭包
    // 占用内存很大的全局变量
    var num = new Array(99999999);
    var num1 = new Array(99999999);
    var num2 = new Array(99999999);
    var num3 = new Array(99999999);
    var num4 = new Array(99999999);
    var num5 = new Array(99999999);
    var num6 = new Array(99999999);
    var num7 = new Array(99999999);

    num = null;


   // 没有及时清理的计时器/定时器
    var intervalId = setInterval(function () {
        console.log("-----");
    }, 1000);

    clearInterval(intervalId);

   // 闭包
    function fn1() {
        var num  = 1111;
        function fn2() {
            num--;
            console.log(num);
        }

        return fn2;
    }

    var f = fn1();
    f();

    f = null;

3、V8下的垃圾回收机制

转载自https://yuchengkai.cn/docs/frontend

V8将内存(堆)分为新生代和老生代两部分

新生代算法

新生代中的对象一般存活时间较短,使用Scavenge GC算法。

在新生代空间中,内存空间分为两部分,分别为From空间和To空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象就会被放入From空间中,当From空间被占满时,新生代GC就会启动了。算法会检查From空间中存活的对象并复制到To空间中,如果有失活的对象就会销毁。当复制完成后将From空间和To空间互换,这样GC就结束了。

老生代算法

老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。

什么情况下对象会出现在老生代空间中:

  • 新生代的对象是否已经经历过一次Scavenge算法,如果经历过得话,会将对象从新生代空间移到老生代空间中
  • To空间的对象占比大小超过25%。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中

老生代中的空间很复杂。

在老生代中,以下情况会先启动标记清除算法:

  • 某一个空间没有分块的时候
  • 空间中被对象超过一定限制
  • 空间不能保证新生代中的对象移动到老生代中

清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象向一端移动,直到所有对象都移动完成然后清理掉不需要的内存。

对象

1、创建对象的方式

1、字面量的方式创建对象

var p1 = {
    name: '张三',
    run: function() {
        console.log(this.name + '跑')
    }
}

2、内置构造函数的方法

var p1 = new Object()
p1.name = '张三'
p1.run = function() {
    console.log(this.name + '跑')
}

问题:使用内置构造函数的方式和字面量的方式来创建对象差不多但都存在以下问题

​ 1、创建的对象无法复用,复用性差

​ 2、如果需要创建多个同类型的对象,需要书写大量重复的代码,代码冗余度高

3、简单工厂函数方式

问题:无法判定类型

class Product {
    constructor(name) {
        this.name = name
    }
    init() {
        alert('init')
    }
}
class Creator {
    ctreate(name) {
        return new Product(name)
    }
}
let creator = new Creator()
let p = creator.create('p1')
p.init()

4、自定义构造函数的方式来创建对象

  • 自定义构造函数和工厂函数的对比

    1、函数的首字母大写(用于区别构造函数和普通函数)

    2、创建对象的过程是由new关键字实现的

    3、在构造函数内部会自动的创建新对象,并赋给this指针

    4、自动返回创建出来的对象

    • 构造函数的执行过程

      function Person(name, age, sex) {
          // 1、自动创建一个空对象,把这个对象的地址 this => 新对象
          // var this = new Object()
          // 2、this给空对象绑定属性和行为
          this.name = name
          this.age = age
          this.sex = sex
          // 3、返回this
          // return this
      }
      var p = new Person()
      
    • 构造函数的返回值说明

      1、如果在构造函数中没有显示的return,则默认返回的是新创建出来的对象

      2、如果在构造函数中显示的return对象,则依照具体的情况处理

      ​ 1>return的是对象,则直接返回这个对象,取而代之本该默认返回的新对象

      ​ 2>return的是null或基本数据类型值,则返回新创建的对象

2、new一个对象的过程

   function Person (name, age, job) {  
        this.name = name;  
        this.age = age;  
        this.job = job;  
        this.sayName = function () {  
            return this.name;  
        }
  }  
  var person = new Person("tom", 21, "WEB");  
  console.log(person.name)

1、创建一个新对象,如:var person = {};

2、新对象的_ proto _属性指向构造函数的原型对象。

3、将构造函数的作用域赋值给新对象。(this对象指向新对象)

4、执行构造函数内部的代码,将属性和方法添加给person中的this对象。

5、返回新对象person。

  var person = {};  
  person._proto_ = Person.prototype;//引用构造函数的原型对象  
  Person.call(person);//将构造函数的作用域给person,即:this值指向person  

  Function.methods("new", function () {  
     //新创建一个对象,它继承了构造器的原型对象。  
     var that = Object.create(this.prototype); //此时,this是指向Function构造器的。  
     //调用构造器,绑定this对象到新对象that上  
     var other = this.apply(that, argument); //此时,this对象指向that对象。  
     //如果它的返回值不是一个对象,就返回新的对象。  
     return (typeof other === "object" && other) || that;  
 });  

通过new关键字创建某构造函数的新实例对象,就是将原型链与实例的this联系起来,this指向这个新对象,同时也指向这个构造函数,并且this对象还是这个构造函数的实例。

对于new来说,还需要注意下运算符优先级。

function Foo() {
    return this;
}
Foo.getName = function () {
    console.log('1');
};
Foo.prototype.getName = function () {
    console.log('2');
};

new Foo.getName(); // 1
new Foo().getName(); // 2

// new Foo()的优先级大于new Foo,所以对于上述代码来说可以这样划分执行顺序
new (Foo.getName());
(new Foo()).getName();

3、删除对象

delete 删除对象中的属性;删除没有使用var关键字声明的全局变量

注意:

1、返回值:布尔类型的值(通过该值判断是否删除成功)

2、使用var关键字声明的变量无法被删除

3、删除对象中不存在的属性没有任何变化,但是返回值为true

4、不能删除window下面的全局变量(使用var声明)但可以删除定义在window上面的属性

4、in、hasOwnProperty、isPrototypeOf和Object.getPrototypeOf()

1、in

判断一个对象是否拥有这个属性。如果对象上没有,就去它的原型对象里面找

function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.address = '杭州';
var p = new Person('csm', 21);
console.log('name' in p);			// true  注意:这里name要加引号表示字符串,不然就是全局变量了
console.log('address' in p);		// true
2、hasOwnProperty

判断当前对象是否拥有这个属性,只到对象自身查找,不查找原型上的属性

function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.address = '杭州';
var p = new Person('csm', 21);
console.log(p.hasOwnProperty('name'));		// true
console.log(p.hasOwnProperty('address'));		// false
3、isPrototypeOf

判断一个对象是否是某个实例的原型对象

function Dog(name, age) {
    this.name = name;
    this.age = age;
}
function Person(name, age) {
    this.name = name;
    this.age = age;
}
var p = new Person('csm', 21);
// 判断p的原型指针是否指向传入构造函数的原型对象,这个过程会往上层层判断。
console.log(Person.prototype.isPrototypeOf(p));		// true
console.log(Object.prototype.isPrototypeOf(p));     // true
console.log(Dog.prototype.isPrototypeOf(p));		// false

5、toString方法、valueOf方法、Symbol.toPrimitive方法

每个对象都有一个toString()方法和valueOf方法

其中toString()方法返回一个表示该对象的字符串,valueOf方法返回该对象的原始值

对于一个对象,toString()返回"[object type]",其中type是对象类型

如果不是对象,toString()返回应有的文本值。

toString()方法和String()方法的区别

toString()方法和String()方法都可以转换为字符串类型。

1、toString()可以将所有的数据都转换为字符串,除了null和undefined

null和undefined调用toString()方法会报错。

如果当前数据为数字类型,则toString()括号中的可以写一个数字,代表进制,可以将数字转化为对应进制字符串。

2、String()可以将null和undefined转换为字符串,但是没法转进制字符串

Symbol.toPrimitive

JavaScript 对象转换到基本类型值时,会使用 ToPrimitive 算法,这是一个内部算法,是编程语言在内部执行时遵循的一套规则。

// 没有 Symbol.toPrimitive 属性的对象
var obj1 = {};
console.log(+obj1);       //NaN
console.log(`${obj1}`);   //"[object Object]"
console.log(obj1 + "");   //"[object Object]"

上面的结果我们可以通过上面说的toSting()方法和value方法去理解。
第一个,+符号。可以看成是是把数据转化为数字类型,由于obj是个空对象,所以结果是NaN
第二个,是es6中的字符串的新语法,这里需要的结果是一个字符串,所以使用的是toString()方法,而toString()方法返回的是对象的类型。
第三个,这里是连接符连接obj。实际上也是需要字符串的结果,所以同理。

// 拥有 Symbol.toPrimitive 属性的对象
var obj2 = {
  [Symbol.toPrimitive](hint) {
    if(hint == "number"){
        return 10;
    }
    if(hint == "string"){
        return "hello";
    }
    return true;
  }
}

console.log(+obj2);     //10    --hint in "number"
console.log(`${obj2}`); //hello --hint is "string"
console.log(obj2 + ""); //"true"

对象在转换基本类型时,首先会调用valueOf然后调用toString。并且这两个方法你是可以重写的。

let a = {
    valueOf() {
        return 0;
    }
}

当然你也可以重写Symbol.toPrimitive,该方法在转基本类型时调用优先级最高。

let a = {
    valueOf() {
        return 0;
    },
    toString() {
        return '1';
    },
    [Symbol.toPrimitive]() {
        return 2;
    }
}
1 + a // 3
'1' + a // '12'

6、对象中属性的遍历

转自ES6核心,教你 玩转 ES6新特性

在ES6中共有5种方法实现对对象属性的遍历

这五种遍历方法都遵循同样的遍历次序规则:

首先遍历所有数值键,按照数值升序排列。

其次遍历所有字符串键,按照加入时间升序排列。

最后遍历所有 Symbol 键,按照加入时间升序排列。

var obj = {name:"feng",age:12}
obj.__proto__.sex="nan"
obj[Symbol()]="bol";
(1).for…in

for…in 是用来遍历自身属性,和继承的可以枚举的属性。

for(var item in obj){
    console.log(item);//name,age,sex
}

// 此时只输出自身属性
for(var item in obj){
    if (obj.hasOwnProperty(item)) {
        console.log(item);//name,age
    }
}
(2).Reflect.ownKeys(obj)

Reflect.ownKeys返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

console.log(Reflect.ownKeys(obj));//[ 'name', 'age', Symbol() ]
(3).Object.keys(obj)

Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。

console.log(Object.keys(obj));//[ 'name', 'age' ]
(4).Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。

console.log(Object.getOwnPropertyNames(obj));//[ 'name', 'age' ]
(5).Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。

console.log(Object.getOwnPropertySymbols(obj));//[ Symbol() ]

原型

1、5个原型规则

  • 所有的引用类型(数组,对象,函数),都具有对象特性,即可自由扩展属性(除了null以外)

  • 所有的引用类型(数组,对象,函数),都有一个_ proto _(隐式原型)属性,属性值是一个普通对象

  • 所有的函数,都有一个prototype(显式原型)属性,属性值也是一个普通对象

  • 所有的引用类型(数组,对象,函数),_ proto _属性值指向他的构造函数的prototype属性

  • 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的_ proto _(即它的构造函数的prototype)

    例:

    // 构造函数
    function Foo(name) {
        this.name = name
    }
    Foo.prototype.alertName = function() {
        alert(this.name)
    }
    // 创建示例
    var f = new Foo('zhangsan')
    f.printName = function () {
        console.log(this.name)
    }
    // 测试
    f.printName()
    f.alertName()
    

    原型链

    示例中 f.alertName()就需要到f._proto_中也就是Foo.prototype(Foo的原型对象)中查找

    f.toString()就要去f._proto_._proto_中查找

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4IXzEXEj-1583129190137)(…\picture\原型链.png)]

2、原型链

  1. 每个函数都能构建出一个对象, 这个对象内部有个属性指向着这个函数的原型对象

  2. 原型对象本质也是一个对象,也是由另外一个构造函数构造出来, 也指向那个构造函数的原型对象,形成一个链式的结构,就称为是原型链

  3. 以上,形成一个链式的结构,就称为是原型链

    数组的完整原型链图

    var arr = [1, 2, 3];
    
    console.log(arr.constructor.name); 								// Array
    
    console.log(arr.__proto__.constructor.name); 					// Array
    
    console.log(Array.__proto__.constructor.name);					// Function
    
    console.log(Function.__proto__.constructor.name);				// Function
    
    console.log(Function.prototype.__proto__.constructor.name);		// Object
    
    console.log(Object.prototype.__proto__);						// null
    console.log(Object.__proto__.constructor.name);					// Function
    
    console.log(Array.prototype.__proto__.constructor.name);		// Object
    

    https://www.processon.com/diagraming/5d808df6e4b0c5c942c450b1

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zKdlhb0x-1583129190138)(…\picture\数组的完整原型链图.png)]

由图可得Function构造函数处于原型链顶端,所有对象的原型对象都由Object构造函数产生

3、一个原型链继承的应用实例

// 写一个封装DOM查询的例子

function Elem(id){
    this.elem = document.getElementById(id);
}

Elem.prototype.html = function(val){
    var elem = this.elem;
    if(val){
        elem.innerHTML = val;
        return this;  // 链式操作
    } else{
        return elem.innerHTML;
    }
}
 
Elem.prototype.on = function(type,fn){
    var elem = this.elem;
    elem.addEventListener(type,fn);
    return this;
}
 
var div1 = new Elem('div');
div1.html('<p>hello imooc</p>').on('click', function(){
    alert('clicked');
}).html('<p>javascript</p>');

继承

1、JavaScript继承的六种方式

另外可以看看这两篇文章:

https://www.cnblogs.com/humin/p/4556820.html

https://www.cnblogs.com/Grace-zyy/p/8206002.html

继承就是让子类拥有父类的资源

继承的意义

​ 减少代码冗余

​ 方便统一操作

​ 弊端

​ 耦合性比较强

继承方法

	// 父类    
	function Person() {
       this.name = 'cc';
       this.pets = ['aa', 'bb'];
    }

    Person.prototype.run = function () {
        console.log('跑');
    };
1. 原型链继承

核心:将父类的实例作为子类的原型

    // 子类        
    function Student() {
        this.num = '111';
    }
	// 让新实例的原型等于父类的实例
    Student.prototype = new Person();

    var stu = new Student();
    console.log(stu.num); // 111
    stu.run(); // 跑

	// 问题:类型问题
	console.log(stu.constructor.name); //Person  对象类型改变

优化:修复constructor指针

    // 子类        
    function Student() {
       this.num = '111';
    }

    Student.prototype = new Person();
    // 修复constructor指针即可
    Student.prototype.constructor = Student;

    var stu = new Student();
    console.log(stu.num);   // 111
    stu.run(); 				// 跑
    console.log(stu.pets);  // ["aa", "bb"]
    console.log(stu.constructor.name);  // Student

	// 问题:继承过来的实例属性, 如果是引用类型, 会被多个子类的实例共享
	var stu1 = new Student();
    stu.pets.push('dd');
    console.log(stu.pets);		// ["aa", "bb", "dd"]
    console.log(stu1.pets);		// ["aa", "bb", "dd"]
2. 借用构造函数继承

核心:在子类型构造函数的内部调用父类构造函数,通过使用call()和apply()方法可以在新创建的对象上执行构造函数。

    // 子类        
    function Student() {
       Person.call(this);
       this.num = '111';
    }
	var stu = new Student();
    console.log(stu.name);   // 111

	// 问题:没用到原型,只能继承父类的实例属性和方法,不能继承原型属性/方法
    stu.run(); 				// 报错:stu.run is not a function
3. 组合继承

核心: 将原型链和借用构造函数的技术组合在一块,从而发挥两者之长的一种继承模式。(常用)

    // 子类        
    function Student() {
       Person.call(this);
       this.num = '111';
    }

    Student.prototype = new Person();
    Student.prototype.constructor = Student;

    var stu = new Student();
    var stu1 = new Student();
    stu.pets.push('小花');
    console.log(stu.pets); 		// ["aa", "bb", "小花"]
    console.log(stu1.pets); 	// ["aa", "bb"]

	// 问题:调用了两次父类构造函数(耗内存)
4. 原型式继承

核心:借助原型,然后基于已有的对象, 创建出新对象;同时不需要创建自定义类型

用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了一个可以随意增添属性的实例或对象。object.create()就是这个原理。

// 原型式继承
function content(obj) {
    function Temp() {}
    Temp.prototype = obj;
    return new Temp();
}
var p = new Person();
var stu1 = content(p);
console.log(stu1.name);
console.log(stu1.age);
5. 寄生式继承

核心:在原型式基础上增强这个对象。所谓增加, 就是指, 再次给这个对象增加一些属性或者方法

    // 子类        
    function Student() {
       this.num = '111';
    }

    function Temp() {}
    Temp.prototype = new Person();
    Student.prototype = new Temp();
    Temp.constructor = Student;

    var stu = new Student();
    console.log(stu);
6. 寄生式组合继承

核心:通过借用函数来继承属性,通过原型链的混成形式来继承方法。(常用)

    // 子类
	function Student(num, name, pets) {
        Person.call(this, name, pets);
        this.num = num;
    }

    function Temp() {}
    Temp.prototype = new Person();
    Student.prototype = new Temp();
    Temp.constructor = Student;

	var stu = new Student('001', '张三', ['小花']);
    var stu1 = new Student('002', '李四', ['小茂']);
    console.log(stu);
    console.log(stu1);

事件

1、事件捕获、事件冒泡、事件委托(代理)

事件捕获:事件从最不精确的对象(document对象)开始触发,然后到最精确

事件冒泡:事件按照从最特定的事件目标到最不特定的事件目标(document对象)的顺序触发

不是所有事件都能冒泡。以下事件不冒泡:blur、focus、load、unload(关闭页面)

事件委托(代理):不在事件的发生地(直接dom)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生元素DOM的类型,来做出不同的响应。

举个例子:

有三个同事预计会在周一收到快递。为签收快递,有两种办法:一是三个人在公司门口等快递;二是委托给前台MM代为签收。现实当中,我们大都采用委托的方案(公司也不会容忍那么多员工站在门口就为了等快递)。前台MM收到快递后,她会判断收件人是谁,然后按照收件人的要求签收,甚至代为付款。这种方案还有一个优势,那就是即使公司里来了新员工(不管多少),前台MM也会在收到寄给新员工的快递后核实并代为签收。

这里其实还有2层意思的:

第一,现在委托前台的同事是可以代为签收的,即程序中的现有的dom节点是有事件的;

第二,新员工也是可以被前台MM代为签收的,即程序中新添加的dom节点也是有事件的。

为什么要用事件委托:

在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的与dom节点进行交互,访问dom的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少DOM操作的原因;如果要用事件委托,就会将所有的操作放到js程序里面,与dom的操作就只需要交互一次,这样就能大大的减少与dom的交互次数,提高性能;

事件委托怎么实现:

举例:最经典的就是ul和li标签的事件监听,比如我们在添加事件时候,采用事件委托机制,不会在li标签上直接添加,而是在ul父元素上添加。

**好处:**比较合适动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制。

// 通用的事件绑定函数
function bindEvent(elem, type, selector, fn) {
    if (fn = null) {
        fn = selector;
        selector = null;
    }
    elem.addEventListener(type, function(e){
        var target;
        if (selector) {
            target = e.target;
            // matches 用来判断当前DOM节点能否完全匹配对应的CSS选择器规则;
            // 如果匹配成功,返回true,反之则返回false。
            if (target.matches(selector)) {
                fn.call(target, e);
            }
        } else {
            fn(e);
        }
    })
}

// 使用代理
var div1 = document.getElementById('div1');
bindEvent(div1, 'click', 'a', function(e){
    console.log(this.innerHtml);
})
// 不使用代理
var a = document.getElementById('a1');
bindEvent(a, 'click', function(e){
    console.log(a.innerHtml);
})

2、事件触发三个阶段

  • 事件捕获阶段:事件从最不精确的对象(document对象)开始触发,然后到最精确
  • 处于目标阶段
  • 事件冒泡阶段:事件按照从最特定的事件目标到最不特定的事件目标(document对象)的顺序触发。不是所有事件都能冒泡。以下事件不冒泡:blur、focus、load、unload(关闭页面)

事件触发一般来说会按照上面的顺序进行,但是也有特例,如果给一个目标节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行。

// 以下会先打印冒泡然后是捕获
node.addEventListener('click', (event) => {
    console.log('冒泡');
}, false);
node.addEventListener('click', (event) => {
    console.log('捕获');
}, true);

通常我们使用addEventListener注册事件,该函数的第三个参数可以是布尔值,也可以是对象。对于布尔值useCapture参数来说,该参数默认值为false。useCapture决定了注册的事件是捕获事件(false)还是冒泡事件(true)。

一般来说,我们只希望事件只触发在目标上,这时候可以使用stopPropagation来阻止事件的进一步传播。通常我们认为stopPropagation是用来阻止事件冒泡的,其实该函数也可以阻止捕获事件。stopImmediatePropagation同样也能实现阻止事件,但是还能阻止该事件目标执行别的注册事件。

3、事件对象

DOM中的事件对象:(符合W3C标准)

preventDefault():取消事件默认行为

stoplmmediatePropagation():取消事件冒泡同时阻止当前节点上的事件处理程序被调用。

stopPropagation():取消事件冒泡对当前节点无影响。

IE中的事件对象

returnValue():取消事件默认行为

cancelBubble():取消事件冒泡

4、mouseover和mouseenter两个事件的区别

mouseover(鼠标覆盖):不论鼠标指针穿过被选元素或其子元素,都会触发 mouseover 事件。

mouseenter(鼠标进入):只有在鼠标指针穿过被选元素时,才会触发 mouseenter 事件。

二者的本质区别在于,mouseenter不会冒泡,简单的说,它不会被它本身的子元素的状态影响到.但是mouseover就会被它的子元素影响到,在触发子元素的时候,mouseover会冒泡触发它的父元素.(想要阻止mouseover的冒泡事件就用mouseenter)

JSON

1、JSON

  • js的对象表示法(JS Object Notation)

  • 存储和交换文本信息的语法,类似XML

  • JSON比XML更小、更快、更易解析

  • 数据格式简单, 易于读写, 占用带宽小

  • // 把对象变成字符串
    JSON.stringify({"a": 10, "b": 20});
    // 把字符串变成对象
    JSON.parse('{"a": 10, "b": 20}');
    

Ajax

1、Ajax,jQuery ajax,axios和fetch的区别

Ajax:

Ajax 即“Asynchronous Javascript And XML”(异步 JavaScript 和 XML),是指一种创建交互式网页应用的网页开发技术。

Ajax 是一种在无需重新加载整个网页的情况下,能够更新部分网页的技术。

通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。这意味着可以在不重新加载整个网页的情况下,对网页的某部分进行更新。

Ajax技术核心就是XMLHttpRequest对象。

(1)设置请求参数(请求方式,请求页面的相对路径,是否异步发送请求)

(2)设置回调函数,一个处理服务器响应的函数,使用 onreadystatechange ,类似函数指针

(3)获取异步对象的readyState 属性:该属性存有服务器响应的状态信息。每当 readyState 改变时,onreadystatechange 函数就会被执行。

(4)判断响应报文的状态,若为200说明服务器正常运行并返回响应数据。

(5)读取响应数据,可以通过 responseText 属性来取回由服务器返回的数据。

var xhr = new XMLHttpRequest();    			// 创建Ajax对象
xhr.open('get', 'aabb.php', true);			// xhr发送请求
xhr.send(null);
xhr.onreadystatechange = function() { 		// xhr获取响应
    if(xhr.readyState == 4) {
        if(xhr.status == 200) {
            console.log(xhr.responseText);
        }
    }
}
 /* ajax返回的状态:
     0:(未初始化)请求还没有建立(open执行前) 
     1:(载入)请求建立了还没发送(执行了open) 
     2:(载入完成)请求正式发送,send()方法执行完成,已经接收到全部响应内容(执行了send) 
     3:(交互)请求已受理,有部分数据可以用,但还没有处理完成,正在解析响应内容
     4:(完成)响应内容解析完成,可以在客户端调用了 
*/ 
Jquery Ajax:

是jQuery框架中的发送后端请求技术,由于jQuery是基于原始的基础上做的封装,所以,jquery Ajax自然也是对原始ajax的封装

$.ajax({
   type: 'POST',
   url: url,
   data: data,
   dataType: dataType,
   success: function () {},
   error: function () {}
});

​ 优缺点:

  • 本身是针对MVC的编程,不符合现在前端MVVM的浪潮
  • 基于原生的XHR开发,XHR本身的架构不清晰,已经有了fetch的替代方案
  • JQuery整个项目太大,单纯使用ajax却要引入整个JQuery非常的不合理(采取个性化打包的方案又不能享受CDN服务)
Fetch:

​ fetch号称是AJAX的替代品,是在ES6出现的,使用了ES6中的promise对象。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多了,参数有点像jQuery ajax。

​ 但是,一定记住fetch不是ajax的进一步封装,而是原生js。Fetch函数就是原生js,没有使用XMLHttpRequest对象。

try {
  let response = await fetch(url);
  let data = response.json();
  console.log(data);
} catch(e) {
  console.log("Oops, error", e);
}
fetch(url).then(response => response.json())
  .then(data => console.log(data))
  .catch(e => console.log("Oops, error", e))

​ 你还可以通过Request构造器函数创建一个新的请求对象,你还可以基于原有的对象创建一个新的对象。 新的请求和旧的并没有什么不同,但你可以通过稍微调整配置对象,将其用于不同的场景。例如:

var req = new Request(URL, {method: 'GET', cache: 'reload'});
fetch(req).then(function(response) {
  return response.json();
}).then(function(json) {
  insertPhotos(json);
});
fetch和ajax 的主要区别

1、fetch()返回的promise将不会拒绝http的错误状态,即使响应是一个HTTP 404或者500

2、在默认情况下 fetch不会接受或者发送cookies

fetch的配置

Promise fetch(String url [, Object options]);
Promise fetch(Request req [, Object options]);

Axios:

​ axios不是原生JS的,需要进行安装,它不但可以在客户端使用,而且可以在nodejs端使用。Axios也可以在请求和响应阶段进行拦截。同样也是基于promise对象的。

https://blog.csdn.net/Roselane_Begger/article/details/88936818

axios({
    method: 'post',
    url: '/user/12345',
    data: {
        firstName: 'Fred',
        lastName: 'Flintstone'
    }
})
.then(function (response) {
    console.log(response);
})
.catch(function (error) {
    console.log(error);
});

优缺点:

  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 客户端支持防止CSRF
  • 提供了一些并发请求的接口(重要,方便了很多的操作)

2、fetch发送post请求时,总是发送两次,第一次状态码204,第二次才成功

因为你用的fetch post修改了请求头,导致fetch第一次发送一个options请求,询问服务器是否支持修改的请求头,如过服务器支持,那么将会再次发送真正的请求。

跨域

1、跨域

什么是跨域

跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制。

浏览器的同源策略是指 协议、域名、端口相同,也就是说 协议、域名、端口有一个不同就算跨域。

PS:http默认端口:80,https默认端口:443

同源策略限制了一下行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取

  • DOM 和 JS 对象无法获取

可以跨越的三个标签:

  • <img src="xxx.png">

  • <link href="xx">

    PS:a和link的区别:a标签属于超链接,用来URL定向;link标签用来连接文件,一般用于CS 文件的引入

  • <script src="xx">

如何实现跨域

1、JSONP:通过动态创建script,再请求一个带参网址实现跨域通信。

ajax请求受同源策略影响,不允许进行跨域请求,而script标签src属性中的链接却可以访问跨域的js脚本,利用这个特性,服务端不再返回JSON格式的数据,而是返回一段调用某个函数的js代码,在src中进行了调用,这样实现了跨域。

为了便于客户端使用数据,逐渐形成了一种非正式传输协议。人们把它称作JSONP,该协议的一个要点就是允许用户传递一个callback参数给服务端,然后服务端返回数据时会将这个callback参数作为函数名来包裹住JSON数据,这样客户端就可以随意定制自己的函数来自动处理返回函数了。

<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
    function jsonp(data) {
    	console.log(data);
	}    
</script>

在开发中可能会遇到多个JSONP请求的回调函数名是相同的,这时候就需要自己封装一个JSONP

function jsonp(url, jsonpCallback, success) {
    let script = document.createElement("script");
    script.src = url;
    script.async = true;
    script.type = "text/javascript";
    window[jsonCallback] = function(data) {
        success & success(data);
    };
    document.body.appendChild(script);
}
jsonp(
	"http://xxx",
    "callback",
    function(value) {
        console.log(value);
    }
);

JSONP的缺点:

  • JSON只支持get,因为script标签只能使用get请求;

  • JSONP需要后端配合返回指定格式的数据。

  • jsonp在调用失败的时候不会返回各种HTTP状态码

  • 安全性。万一假如提供jsonp的服务存在页面注入漏洞,即它返回的javascript的内容被人控制的

ajax与jsonp的异同

  • ajax和jsonp这两种技术再调用方式上“看起来很像”,目的也一样,都是请求一个url,然后把服务器返回的数据进行处理,因此jquery和ext等框架都把jsonp作为ajax的一种形式进行了封装。
  • 但ajax和jsonp在本质上是不同的东西。ajax的核心是通过XmlHttpRequest获取非本页内容,而jsonp的核心则是动态添加。

2、document.domain + iframe跨域:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

该方式只能用于二级域名相同的情况下,比如a.test.com和b.test.com适用于该方法。

只需要给页面添加document.domain = 'test.com’表示二级域名相同就可以实现跨域。

3、location.hash + iframe跨域:a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

4、window.name + iframe跨域:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。

5、postMessage跨域:可以跨域操作的window属性之一。

postMessage和iframe相结合的方法。postMessage(data,origin)方法允许来自不同源的脚本采用异步方式进行通信,可以实现跨文本档、多窗口、跨域消息传递。

这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息。

// 发送消息端
window.parent.postMessage('message', 'http://test.com');
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {
    var origin = event.origin || event.originalEvent.orign;
    if (origin === 'http://test.com') {
        console.log('验证通过');
    }
})

6、CORS:(服务器端设置http header)服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求,前后端都需要设置。

// 第二个参数填写允许跨域的域名称,不建议直接写"*"
response.setHeader("Access-Control-Allow-Origin", "http://a.com, http://b.com");
response.setHeader("Access-Control-Allow-Headers", "X-Requested-With");
response.setHeader("Access-Control-Allow-Methods", "PUT, POST, GET, DELETE, OPTIONS");
// 接收跨域的cookie
response.setHeader("Access-Control-Allow-Credential", "true")

7、代理跨域:起一个代理服务器,实现数据的转发。

异步

1、defer和async

延迟脚本

<script type="text/javascript" defer="defer" src="example.js"></script>

defer属性:表明脚本在执行时不会影响页面的构造,也就是说,脚本会被延迟到整个页面都解析完毕再运行

异步脚本

HTML5为<script>定义了async属性。与defer属性相似,都用于改变处理脚本的行为。脚本异步执行,但标记为async的脚本并不会保证按照指定它们的先后顺序执行

<script type="text/javascript" async src="example.js"></script>

指定async属性的目的是不让页面等待两个脚本下载和执行,从而异步加载页面其他内容。为此,建议异步脚本不要在加载期间修改DOM。

异步脚本一定会在页面的load时间前执行,但可能会在DOMContentLoaded事件触发之前或之后执行。

2、单线程

概念:只有一个线程,同一时间只能做一件事

// alert不处理,js执行和DOM渲染暂时卡顿
console.log(100)
alert(200)
console.log(300)

原因:避免DOM渲染的冲突

解决方案:js是单线程的,一次只能完成一个任务,如果有多个任务,就需要排队,如果有一个任务耗时很长,那么后边任务就需要等待。为了解决这个问题,js将任务的执行分成两种模式:同步和异步

ps:Web Worker(HTML5)可以为 JavaScript 创造多线程环境,但是不能访问DOM。具体可以看 Web Worker 使用教程 - 阮一峰

3、同步和异步

区别:是否阻塞后面程序的进行

// 异步
console.log(100)
setTimeout(function() {
    console.log(200)
}, 1000)
console.log(300)

// 100 300 200

异步的缺点:

1、没按照书写方法执行,可读性较差

2、callback中不容易模块化

// 同步
console.log(100)
alert(200)  //只有点击了确认按钮,后面的程序才会接着执行
console.log(300)

使用异步的场景

  • 定时任务:setTimeout、setInterval
  • 网络请求:ajax请求、动态<img>加载
  • 事件绑定

4、事件轮询(event-loop)

事件轮询是JS实现异步的具体解决方案。

js将所有任务分为两类:同步任务和异步任务。

事件轮询过程:同步任务在主线程上形成执行栈排队执行,异步的函数先放在任务队列中(不一定马上),当主线程执行完所有代码的时候,会从这个任务队列里面取任务,如果有就执行。

  • 执行栈中的同步任务执行完毕,即主线程空闲时,此时系统会去查看任务队列,如果有可运行的异步任务就将任务添加到执行栈中开始执行。
  • 如果任务队列中有多个异步任务待执行,那么这些异步任务也要排队等待被执行,并不是添加到任务队列中的异步任务就会立即执行。
  • 只要主线程空了,就会去读取任务队列,这个过程是循环不断的,这就是javaScript的运行机制,也称作事件循环。

例:

setTimeout(function() {
    console.log(1)
}, 100)
setTimeout(function() {
    console.log(2)
})
console.log(3)
$.ajax({
    url: 'xxx',
    success: function(result) {
        console.log(4)
    }
})

// 任务队列
	// 主进程
		console.log(3)
	// 异步队列
        // 立即被放入
        function() {
            console.log(2)
        }
        // 100ms之后放入
        function() {
            console.log(1)
        }
        // ajax加载完成时被放入(时间不确定)
        function() {
            console.log(4)
        }

一般说:100ms后会执行setTimeout里的回调函数,这样的说法并不严谨

准确的说是:第一个setTimeout在100ms后才会被放到异步队列中,第二个setTimeout会立刻放入异步队列,而添加到异步队列的任务,只有等到主线程执行栈中的同步任务全部执行完毕之后,才会被执行。如果主线程执行任务很多,执行时间超过100ms,那么这个函数只能等待。

5、微任务和宏任务

不同的任务源会被分配到不同的任务队列中,任务源可以分为微任务(microtask)和宏任务(macrotask)。在ES6规范中,microtask称为jobs,macrotask称为task。

  • 微任务包括process.nextTick,promise,Object.observe,MutationObserver

  • 宏任务包括script,setTimeout,setInterval,setImmediate,requestAnimationFrame,I/O,UI rendering

console.log('script start');

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 => Promise => script end => promise1 => promise2 => setTimeout

虽然setTimeout写在Promise之前,但是Promise属于微任务而setTimeout属于宏任务。Promise.then是异步执行的,而创建Promise实例(executor)是同步执行的。

注意:Promise不等于异步,整个promise执行过程是同步的,Promise只是个异步操作容器,把异步操作包起来,以更易读更有条理更符合人类思维习惯的方式来写异步代码。

可以看看这篇文章:Promise≠异步,async函数≠异步

很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了script,浏览器会先执行一个宏任务,接下来是异步代码的话就先执行微任务。

所以正确的一次Event loop顺序是这样的

1、执行同步代码,这属于宏任务

2、执行栈为空,查询是否有微任务需要执行

3、执行所有微任务

4、必要的话渲染UI

5、然后开始下一轮Event loop,执行宏任务中的异步代码

6、Node.js的Event Loop

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

Node.js的运行机制如下。

(1)V8引擎解析JavaScript脚本。

(2)解析后的代码,调用Node API。

(3)libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。

(4)V8引擎再将结果返回给用户。

除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与"任务队列"有关的方法:process.nextTicksetImmediate。它们可以帮助我们加深对"任务队列"的理解。

process.nextTick方法可以在当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。

setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。请看下面的例子(via StackOverflow)。

process.nextTick(function A() {
    console.log(1);
    process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
	console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED

上面代码中,由于process.nextTick方法指定的回调函数,总是在当前"执行栈"的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。

现在,再看setImmediate。

setImmediate(function A() {
    console.log(1);
    setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
    console.log('TIMEOUT FIRED');
}, 0);

上面代码中,setImmediate与setTimeout(fn,0)各自添加了一个回调函数A和timeout,都是在下一次Event Loop触发。那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是1–TIMEOUT FIRED–2,也可能是TIMEOUT FIRED–1--2。

我们由此得到了process.nextTick和setImmediate的一个重要区别:多个process.nextTick语句总是在当前"执行栈"一次执行完,多个setImmediate可能则需要多次loop才能执行完。

7、处理异步的几种方法

1、回调函数

回调(callback)是一个函数被作为一个参数传递到另一个函数里,在那个函数执行完后再执行。( B函数被作为参数传递到A函数里,在A函数执行完后再执行B )

假定有两个函数f1和f2,f2等待f1的执行结果,f1()–>f2();如果f1很耗时,可以改写f1,把f2写成f1的回调函数:

function f1(callback){
  setTimeout(function () {
    callback(); // f1的任务代码
  }, 1000);
}
f1(f2);  // 执行

优点:简单,方便,易用

缺点:不利于代码的阅读和维护,易形成回调函数地狱,各个部分之间高度耦合,流程会很混乱

什么是回调地域?

如果我们只有一个异步操作,用回调函数来处理是完全没有任何问题的。如果我们在回调函数中再嵌套一个回调函数,问题也不大。但是如果我们要嵌套很多个回调函数,问题就很大了,因为多个异步操作形成了强耦合,代码将乱作一团,无法管理。这种情况被称为"回调函数地狱"(callback hell)。

$.ajax(url, () => {
	//do something...
	$.ajax(url1, () => {
		//do something...
		$.ajax(url2, () => {
			//do something...
            $.ajax(url3, () => {
                //do something...
            })
		})
	})
})

注意 区分 回调函数和异步,回调是实现异步的一种手段,并不一定就是异步。

回调也可以是同步,如:

function A(callback){
    callback();  //调用该函数
    console.log("I am A");
}
function B(){
   console.log("I am B");
}
A(B);
// I am B
// I am A
2、事件监听

采用事件驱动模式,任务的执行不取决于代码的顺序,而取决于某个事件是否发生。

监听函数有:on,bind,listen,addEventListener,observe

<div id="id1">
    <div id="id2">111</div>
</div>

<script>
    document.getElementById("id1").addEventListener("click",function(){console.log('id1');});
	document.getElementById("id2").addEventListener("click",function(){console.log('id2');});
	document.getElementById("id2").addEventListener("click",function(){console.log('id2222');});
</script>

// id2 id2222 id1

优点:与回调函数相比,可绑定多个事件,每一个事件可指定多个回调函数,事件监听实现了代码的解耦,有利于实现模块化

缺点:使用不方便,整个程序都要变成事件驱动型,每次都要手动地绑定和触发事件

3、Promise
promise标准
  • 三种状态:pending(未加载)、resolved/fulfilled(成功)、rejected(失败)
  • 初始状态pending可变为fulfilled或rejected,且状态变化不可逆
  • promise 必须实现 then 方法(可以说,then 就是 promise 的核心),而且 then 必须返回一个 promise,同一个 promise 的 then 可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致
  • then 方法接受两个参数,第一个参数是成功时的回调(resolve),另一个是失败时的回调(reject)。同时,then 可以接受另一个 promise 传入,也接受一个 “类 then” 的对象或方法,即 thenable 对象。
promise的使用

promise封装api 基本使用

​ 1>new Promise实例,而且要return

​ 2>new Promise时要传入函数,函数有resolve,reject两个函数

​ 3>成功时执行resolve,失败时执行reject

​ 4>then监听结果

function loadImg(src) {
    const promise = new Promise(function(resolve, reject) {
        var img = document.createElement('img')
        img.onload = function() {
            resolve(img)
        }
        img.onerror = function() {
            reject()
        }
        img.src = src
    })
    return promise
}

var src = 'xxx.png'
var result = loadImg(src)
result.then(function(img) {
    console.log(img.width)
}, function() {
    console.log('failed')
})

简单实现promise
import StateMachine from 'javascript-state-machine'

// 定义promise
class MyPromise {
    constructor(fn) {
        this.successList = []
        this.failList = []
        fn(() => {
            // resolve函数
            fsm.resolve(this)
        }, () => {
            // reject函数
            fsm.reject(this)
        })
    }
    then(successFn, failFn) {
        this.successList.push(successFn)
        this.failList.push(failFn)
    }
}
// 状态机模型
var fsm = new StateMachine({
    init: 'pending',
    transitions: [
        {
            name: 'resolve',
            from: 'pending',
            to: 'fullfilled'
        },
        {
            name: 'reject',
            from: 'pending',
            to: 'rejected'
        }
    ],
    methods: {
        // 成功
        onResolve: function(state, data) {
            // 参数:state-当前状态  示例:data-fsm.resolve(xx)执行时传递过来的参数
            data.successList.forEach(fn => fn())
        },
        // 失败
        onReject: function(state, data) {
            // 参数:state-当前状态  示例:data-fsm.reject(xx)执行时传递过来的参数
            data.failList.forEach(fn => fn())
        },
        
    }
})

下面转自http://www.alloyteam.com/2014/05/javascript-promise-mode/

源码:https://github.com/chemdemo/promiseA/blob/master/lib/Promise.js

简单分析下思路:

构造函数 Promise 接受一个函数 resolver,可以理解为传入一个异步任务,resolver 接受两个参数,一个是成功时的回调,一个是失败时的回调,这两参数和通过 then 传入的参数是对等的。

其次是 then 的实现,由于 Promise 要求 then 必须返回一个 promise,所以在 then 调用的时候会新生成一个 promise,挂在当前 promise 的_next 上,同一个 promise 多次调用都只会返回之前生成的_next

由于 then 方法接受的两个参数都是可选的,而且类型也没限制,可以是函数,也可以是一个具体的值,还可以是另一个 promise。下面是 then 的具体实现:

Promise.prototype.then = function(resolve, reject) {
    var next = this._next || (this._next = Promise());
    var status = this.status;
    var x;
 
    if('pending' === status) {
        isFn(resolve) && this._resolves.push(resolve);
        isFn(reject) && this._rejects.push(reject);
        return next;
    }
 
    if('resolved' === status) {
        if(!isFn(resolve)) {
            next.resolve(resolve);
        } else {
            try {
                x = resolve(this.value);
                resolveX(next, x);
            } catch(e) {
                this.reject(e);
            }
        }
        return next;
    }
 
    if('rejected' === status) {
        if(!isFn(reject)) {
            next.reject(reject);
        } else {
            try {
                x = reject(this.reason);
                resolveX(next, x);
            } catch(e) {
                this.reject(e);
            }
        }
        return next;
    }
};

Pomise.all的使用

全部完成之后统一执行

例1:

let p1 = new Promise((resolve, reject) => {
  resolve('成功了')
})

let p2 = new Promise((resolve, reject) => {
  resolve('success')
})

let p3 = Promise.reject('失败')

Promise.all([p1, p2]).then((result) => {
  console.log(result)               //['成功了', 'success']
}).catch((error) => {
  console.log(error)
})

Promise.all([p1,p2,p3]).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)      // 失败了,打出 '失败'
})

例2:

let wake = (time) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`${time / 1000}秒后醒来`)
    }, time)
  })
}

let p1 = wake(3000)
let p2 = wake(2000)

Promise.all([p1, p2]).then((result) => {
  console.log(result)       // [ '3秒后醒来', '2秒后醒来' ]
}).catch((error) => {
  console.log(error)
})

需要特别注意的是,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,即p1的结果在前,即便p1的结果获取的比p2要晚。这带来了一个绝大的好处:在前端开发请求数据的过程中,偶尔会遇到发送多个请求并根据请求顺序获取和使用数据的场景,使用Promise.all毫无疑问可以解决这个问题。

简单实现 Promise.all
function promiseAll(promises) {
    return new Promise((resolve, reject) => {
      let resultCount = 0;
      let results = new Array(promises.length);

      for (let i = 0; i < promises.length; i++) {
        promises[i].then(value => {
          resultCount++;
          results[i] = value;
          if (resultCount === promises.length) {
            return resolve(results)
          }
        }, error => {
          reject(error)
        })
      }
    })
  }

  let p1 = new Promise(resolve => resolve('p1'))
  let p2 = new Promise(resolve => resolve('p2'))
  let p3 = Promise.reject('p3 error')

  promiseAll([p1, p2]).then(results => {
    console.log(results)    // ['p1', 'p2']
  }).catch(error => {
    console.log(error)
  })

  promiseAll([p1, p2, p3]).then(results => {
    console.log(results)
  }).catch(error => {
    console.log(error)      // 'p3 error'
  })

Promise.race的使用

只要有一个完成就执行,返回为最先完成的

let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  },1000)
})

let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('failed')
  }, 500)
})

Promise.race([p1, p2]).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)  // failed
})

异常捕获

包括语法错误,逻辑错误(如图片加载错误)

// 规定:then只接受一个参数,最后统一用catch捕获异常
result.then(function (img) {
    console.log(img.width)
    return img // 防止出现异常
}).then(unction (img) {
    console.log(img.height)
}).catch(function(err) {  // 最后统一catch,统一捕获异常
    console.log(err)
})

优点:将回调函数嵌套调用变成了链式调用,逻辑更强,执行顺序更清楚,很好地体现了开放封闭原则

缺点:代码冗余,异步操作都被包裹在Promise构造函数和then方法中,主题代码不明显,语义不清楚

4、async/await

概念

  • async/await是写异步代码的新方式
  • async/await是基于Promise实现的,它不能用于普通的回调函数及节点回调
  • async/await和Promise一样,是非阻塞的
  • async/await使得异步代码看起来像同步代码
  • async函数是generrator函数的语法糖,它相当于一个自带执行器的generator函数

用法

  • 使用await函数必须用async标识
  • await后面跟的是一个Promise实例
  • 任何一个async函数都会隐式返回一个Promise,并且promise resolve的值就是return返回的值
async function test() {
    return "1";
}
console.log(test());  // Promise {<resolved>: "1"}

  • await不处理异步error

    await是不管异步过程的reject(error)消息的,async函数返回的这个Promise对象的catch函数负责统一抓取内部所有异步过程的错误;async函数内部只要有一个异步过程发生错误,整个执行过程就中断,这个返回的Promise对象的catch就能抓取到这个错误

使用例子

JavaScript 利用 async await 实现 sleep 效果

function sleep() {
    return new Promise(resolve => {
        setTimeout(() => {
            console.log('finish');
            resolve("sleep");
        }, 2000);
    });
}
async function test() {
    let value = await sleep();
    console.log(value);
}
test();
// finish
// sleep

因为await会等待sleep函数resolve,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。它等待后面的promise对象执行完毕,然后拿到promise resolve 的值并进行返回,返回值拿到之后,它继续向下执行。

优点:从上到下,顺序执行,就像写同步代码一样,更符合代码编写习惯,最适合处理多个Promise异步操作

缺点:滥用await可能会导致性能问题,因为await会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致失去了并发性

5、generator(不是异步的直接替代方案)

参考:https://blog.csdn.net/yuefujuan_1992/article/details/89055602

generator因其中断/恢复执行和传值等优秀功能被人们用于异步处理

协程:多个线程相互协作,完成异步任务

定义:generator函数是协程在ES6的实现,最大特点就是可以交出函数的执行权(即暂停执行)。整个generator函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。

执行:返回一个内部指针,不会返回结果,返回的是指针对象{value:**,done:‘true/false’}

Generator函数之所以可以用于异步操作是因为yield关键字,Generator函数在执行过程中遇到yield语句时就会暂停执行,并返回yield语句后面的内容,要想继续执行后续的代码就需要手动调用next方法。这样就找到了顺序执行异步操作的方法了,也就是将所有异步操作都放在yield关键字后面,同时在异步操作内配置相应的next方法,以便在异步操作结束后返回出操作的结果并交出执行权

形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}
var hw = helloWorldGenerator();

以上代码定义了一个generator函数helloWorldGenerator,内部有两个yield表达式(“hello” “world”),和一个return语句(结束执行)。

调用helloWorldGenerator函数后,并不执行,而是返回一个遍历器对象(lterator),它是一个指向内部状态的指针。

然后可以调用遍历器对象的next()方法,每次调用该方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句或return语句为止。

换言之,generator函数是分段执行的,yield表达式是暂停执行的标记,next方法可以恢复执行。

hw.next()
// { value: 'hello', done: false }
 
hw.next()
// { value: 'world', done: false }
 
hw.next()
// { value: 'ending', done: true }
 
hw.next()
// { value: undefined, done: true }

第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。

第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。

第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。

第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。

总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

generator异步读取文件

var fs = require('fs');
var readfile = function(filename){
  return new Promise((resolve, reject=>{
    fs.readFile(filename, (error,data)=>{
        if(error) reject(new Error('whoops');
        resolve(data);
      });
  });
}
//generator
var gen=function* (){
  var file1=yield readfile('./data1.json');
  var file2= yield readfile('./data2.json');
  console.log('done');
}
//calling code
var generator=gen();
generator.next().value.then(data=>{
  generator.next(data).value.then((data)=>{
      generator.next(data);
    }
  );
});

generator函数和async函数的区别
1、generator函数被调用后返回一个遍历器对象(lterator),async函数返回一个promise对象
2、async函数内置执行器,可以像普通函数那样调用,generator函数需要使用co模块来实现流程控制或者自定义流程控制。
3、async是generator的语法糖

模块化

1、模块化

**概念:**将一个复杂的程序依据一定的规则(规范)封装成几个块(文件)并进行组合。

模块的内部数据的实现是私有的,只是向外部暴露一些接口(方法)与外部其他模块通信,这就是模块化。

**优点:**模块化可以降低代码耦合度,减少重复代码,提高代码重用性,并且在项目结构上更加清晰,便于维护。

AMD、CMD、CommonJs、ES6的对比

他们都是用于在模块化定义中使用的,AMD、CMD、CommonJs是ES5中提供的模块化编程的方案,import/export是ES6中定义新增的

AMD

AMD是RequireJS在推广过程中对模块定义的规范化产出,它是一个概念,RequireJS是对这个概念的实现,就好比JavaScript语言是对ECMAScript规范的实现。AMD是一个组织,RequireJS是在这个组织下自定义的一套脚本语言

RequireJS:是一个AMD框架,可以异步加载JS文件,按照模块加载方法,通过define()函数定义第一个参数是一个数组,里面定义一些需要依赖的包,第二个参数是一个回调函数,通过变量来引用模块里面的方法,最后通过return来输出。

是一个依赖前置、异步定义的AMD框架(在参数里面引入js文件),在定义的同时如果需要用到别的模块,在最前面定义好即在参数数组里面进行引入,在回调里面加载

define(['./a', './b'], function(a, b) {
    a.do();
    b.do();
});
define(function(require,exports,module){
	var a = require('./a');
    a.doSomething();
    var b = require('./b');
    b.doSomething();
});
CMD

SeaJS在推广过程中对模块定义的规范化产出,是一个同步模块定义,是SeaJS的一个标准,SeaJS是CMD概念的一个实现,SeaJS是淘宝团队提供的一个模块开发的js框架.

通过define()定义,没有依赖前置,通过require加载jQuery插件,CMD是依赖就近,在什么地方使用到插件就在什么地方require该插件,即用即返,这是一个同步的概念

define(id?, deps?, factory)

factory是一个函数,有三个参数,function(require, exports, module)

  1. require 是一个方法,接受 模块标识 作为唯一参数,用来获取其他模块提供的接口:require(id)
  2. exports 是一个对象,用来向外提供模块接口
  3. module 是一个对象,上面存储了与当前模块相关联的一些属性和方法
// 定义模块  module.js
define(function(require, exports, module) {
  var $ = require('jquery.min.js')
  $('div').addClass('active');
});

// 加载模块
seajs.use(['module.js'], function(my){
});
AMD与CMD区别

AMD和CMD最明显的区别就是在模块定义时对依赖模块的执行时机处理不同。

1、AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块

js可以方便知道依赖模块是谁,立即加载;

2、CMD推崇就近依赖,只有在用到某个模块的时候再去require

需要使用把模块变为字符串解析一遍才知道依赖了那些模块,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。

ES6

ES6模块的主要有两个功能:export和import。

  • export用于对外输出本模块(一个文件可以理解为一个模块)变量的接口。

  • import用于在一个模块中加载另一个含有export接口的模块。

也就是说使用export命令定义了模块的对外接口以后,其他JS文件就可以通过import命令加载这个模块(文件)。

//util1.js
export default {
    a: 100
}

//index.js
import util1 from './util1.js'
console.log(util1);
//util2.js
export function fn1() {
    alert('fn1');
}
export function fn2() {
    alert('fn2');
}

//index.js
import { fn1, fn2 } from './util2.js'
fn1();
fn2();
CommonJs

是通过module.exports定义的,在前端浏览器里面并不支持module.exports,通过node.js后端使用的。Nodejs端是使用CommonJS规范的,前端浏览器一般使用AMD、CMD、ES6等定义模块化开发的。

加载模块使用require方法,该方法读取一个文件并执行,最后返回文件内部的exports对象。

CommonJS定义的模块分为:{模块引用(require)} {模块定义(exports)} {模块标识(module)}

  • require()用来引入外部模块;

  • exports对象用于导出当前模块的方法或变量,唯一的导出口;

  • module对象就代表模块本身。

// a.js
module.exports = {
    a: 1;
}
// or
exports.a = 1;

// b.js
var module = require('./a.js');
module.a; // 1
commonjs和ES6模块化的区别
  • 前者支持动态导入,也就是require(${path}/xx.js),后者目前不支持,但是已有提案
  • 前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用导入会对渲染有很大影响
  • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化。
  • 后者会编译成require/export来执行

2、export和export default的区别

  • export default命令用于指定模块的默认输出。

  • 显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。

  • 所以import命令后面才不用加{},因为只可能唯一对应export default命令。

本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。

1、export与export default均可用于导出常量、函数、文件、模块等。

2、在一个文件或模块中,export、import可以有多个,export default仅有一个。

3、通过export 方法导出,在导入时要加{},export default则不需要。

4、export,import时需要知道所加载的变量名或函数名;export default,import时可指定任意名字

5、(1)输出单个值,只用export default

​ (2)输出多个值,使用export

​ (3)export default与普通export不要同时使用

3、webpack和gulp区别(模块化与流的区别)

webpack是一个前端模块化方案,更侧重模块打包,我们可以把开发中的所有资源(图片、js文件、css文件等)都看成模块,通过loader(加载器)和plugins(插件)对资源进行处理,打包成符合生产环境部署的前端资源。

webpack概念与配置

gulp强调的是前端开发的工作流程,我们可以通过配置一系列的task,定义task处理的事务(例如文件压缩合并、雪碧图、启动server、版本控制等),然后定义执行顺序,来让gulp执行这些task,从而构建项目的整个前端开发流程。

性能优化

1、性能优化

原则:多使用内存,缓存或其他方式。减少CPU计算,减少内存。

1. 采用 lazyLoad

俗称懒加载,可以控制网页上的内容在一开始无需加载,不需要发请求,等到用户操作真正需要的时候立即加载出内容。这样就控制了网页资源一次性请求数量。如图片懒加载,下拉加载更多。

2. 控制资源文件加载优先级

浏览器在加载HTML内容时,是将HTML内容从上至下依次解析,解析到link或者script标签就会加载href或者src对应链接内容,为了第一时间展示页面给用户,就需要将CSS提前加载,不要受 JS 加载影响。

一般情况下都是CSS在头部,JS在底部。

3. 利用浏览器缓存

浏览器缓存是将网络资源存储在本地,等待下次请求该资源时,如果资源已经存在就不需要到服务器重新请求该资源,直接在本地读取该资源。

4. 减少 DOM 操作

JS对DOM的操作优化法则

(1)批量读写
当我们需要获取某一属性,这一属性需要计算才能得到,并且队列中存在尚未提交的DOM修改操作,则此时,DOM修改操作的队列将会被提交。
为了提高效率,减少更新render tree的次数,可以先统一读取属性,然后统一修改DOM,这样,就可以减少更新render tree的次数。

var listNode = document.getElementById('list')
// 插入10个li标签
var frag = document.createDocumentFragment() // 先定义一个片段
var m, li
for (m = 0; m < 10; m++) {
    li = document.createElement("li")
    li.innerHTML = "List Item" + x
    frag.appendChild(li)					// 将li插入片段
}
listNode.appendChild(frag)					// 只执行一次DOM操作

(2)虚拟结点
当我们需要对DOM做出大量修改时,可以先创建一个虚拟结点,将所有修改附加在该节点,最后将该虚拟节点一次性提交给在render tree上存在的结点,则相当于只提交了一次修改操作。

(3)查找元素的优化
因为ID是唯一的,也有原始的方法,因此使用ID查找元素是最快的,其次的是根据类和类型查找元素,通过属性查找元素是最慢的,因此应该尽可能的通过ID或者类来查找元素,避免通过类来查找元素。

5. DNS预解析

DNS解析也是需要时间的,可以通过与解析的方式来预先获得域名所对应的IP

<link rel="dns-prefetch" href="//yuchengkai.cn">

6. 图片加载优化
  • 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用CSS去代替

  • 图标用Iconfont代替

  • 一般图片都用CDN加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片

    CDN图片加速服务是指CDN网络和客户源文件服务器形成良好的互动,即将源站的图片、flash动画、css / js、及各种文件类型的图片缓存于CDN中心网络中,这些文件的特点在于更新的频率较低,用缓存技术将文件cache在CDN的边缘节点上,即可满足终端用户就近访问的需求。文件可以通过定期和不定期的方式在CDN节点上进行更新:定期更新时CDN中心网络主动更新源站数据,再通过智能解析系统将内容进行优化分配到各CDN网络节点;不定期更新可以通过CDN客服管理系统进行主动推送完成。

    CDN图片加速服务是当前CDN服务的基本应用,也是使用最为广泛的服务。例如在线商城,图片站,图库,地图导航,新闻资讯,行业门户等,为了提高图片在客户端的加载速度,都会使用CDN图片加速服务。

  • 小图使用base64格式

  • CSS Sprites,将多个图标文件整合到一张图片中(雪碧图/精灵图)

  • 选择正确的图片格式:

    • 对于能够显示WebP格式的浏览器尽量使用WebP格式。因为WebP格式具有更好的图象数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图象质量,缺点就是兼容性并不好
    • 小图使用PNG,其实对于大部分图标这类图片,完全可以用SVG代替
    • 照片使用JPEG
7. 使用Webpack优化项目
  • 对于Webpack4,打包项目使用production模式,这样会自动开启代码压缩
  • 使用ES6模块来开启tree shaking,这个技术可以移除没有使用的代码
  • 优化照片,对于小图可以使用base64的方式写入文件中
  • 按照路由拆分代码,实现按需加载
  • 给打包出来的文件名添加哈希,实现浏览器缓存文件
8. 使用SSR后端渲染,数据直接输入到HTML
9. 事件节流

2、图片懒加载和预加载

参考:https://www.cnblogs.com/psxiao/p/11542930.html

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

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

两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。

懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

懒加载:

img的data-src属性及懒加载:当访问一个页面的时候,先把img元素或是其他元素的背景图片路径替换成一张大小为1*1px图片的路径(这样就只需要请求一次),当图片出现在浏览器的可视区域内时,才设置图片真正的路径,让图片显示出来。这就是图片懒加载。

通俗一点:

1、就是创建一个自定义属性data-src存放真正需要显示的图片路径,而img自带的src放一张为1*1px的图片路径。

2、当页面滚动直至此图片出现在可视区域时,用js取到该图片的data-src的值赋给src。

ps:自定义属性可以取任何名字

**场景:**对于图片过多的页面,为了加快页面加载速度,需要将页面内未出现的可视区域内的图片先不做加载,等到滚动可视区域后再去加载。

**原理:**img标签的src属性用来表示图片的URL,当这个属性值不为空时,浏览器就会根据这个值发送请求,如果没有src属性就不会发送请求。所以,在页面 加入时将img标签的src指向为空或者指向一个小图片(loading或者缺省图),将真实地址存在一个自定义属性data-src中,当页面滚动时,将可视区域的图片的src值赋为真实的值。

预加载:

简单理解:就是在使用该图片资源前,先加载到本地来,真正到使用时,直接从本地请求数据就行了。

var arr = [
    '../picture/1.jpg',
    '../picture/2.jpg',
    '../picture/3.jpg',
];

var imgs =[]
preLoadImg(arr);

//图片预加载方法
function preLoadImg(pars){
    for(let i=0;i<arr.length;i++){
        imgs[i] = new Image();
        imgs[i].src = arr[i];
    }
}

预加载其实是声明式的fetch,强制浏览器请求资源,并且不会阻塞onload事件,可以试用以下代码开启预加载。

<link rel="preload" href="http://example.com"></link>

预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好。

3、优化SPA应用的首屏加载速度慢的问题

  • 将公用的JS库通过script标签外部引入,减小 app.bundle 的大小,让浏览器并行下载资源文件,提高下载速度;
  • 在配置路由时,页面和组件使用懒加载的方式引入,进一步缩小 app.bundle 的体积,在调用某个组件时再加载对应的js文件;
  • 加一个首屏loading图,提升用户体验;

4、防抖和节流

防抖

在日常开发中,像在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作这些需求都可以通过函数防抖动来实现。

整体实现:

  • 对于按钮防点击来说的实现:一旦我开始一个定时器,只要定时器还在,不管你怎么点击都不会执行回调函数。一旦定时器结束并设置为null,就可以再次点击了。
  • 对于延时执行函数来说的实现:每次调用防抖动函数都会判断本次调用和之前的时间间隔,如果小于需要的时间间隔,就会重新创建一个定时器,并且定时器的延迟为设定时间减去之前的时间间隔。一旦时间到了,就会执行相应的回调函数。
节流

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。比如搜索框就会用到节流。

不知道怎么分类其他

1、获得一段范围内的随机数

值 = Math.floor(Math.random()*可能值的总数+第一个可能的值)

例:选择一个1-10之间的数组

var num = Math.floor(Math.random()*10+1)

2、获取xxxx-xx-xx格式的日期

function formatDate(dt) {
    if (!dt) {
        dt = new Date()
    }
    var year = dt.getFullYear();
    var month = dt.getMonth() + 1;
    var date = dt.getDate();
    if (month < 10) {
        month = '0' + month;  // 强制类型转换
    }
    if (date < 10) {
        date = '0' + date; // 强制类型转换
    }
    return year + '-' + month + '-' + date;
}

var dt = new Date();
console.log(formatDate(dt));

3、将时间戳转化为自定义日期格式

date.js

export function formatDate (date, fmt) {
    if (/(y+)/.test(fmt)) {
        fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
    }
    let o = {
        'M+': date.getMonth() + 1,
        'd+': date.getDate(),
        'h+': date.getHours(),
        'm+': date.getMinutes(),
        's+': date.getSeconds()
    };
    for (let k in o) {
        if (new RegExp(`(${k})`).test(fmt)) {
            let str = o[k] + '';
            fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? str : padLeftZero(str));
        }
    }
    return fmt;
};

function padLeftZero (str) {
    return ('00' + str).substr(str.length);
};
<template>
    <!-- 过滤器  time 可以使后台得到的数据,循环出来的也行 -->
    <div>{{time | formatDate}}</div>
    <!-- 输出结果 -->
    <!-- <div>2016-07-23 21:52</div> -->
</template>
<script>
import {formatDate} from './common/date.js';
export default {
    filters: {
        formatDate(time) {
            var date = new Date(time);
            return formatDate(date, 'yyyy-MM-dd hh:mm');
        }
    }
}
</script>

4、写一个能遍历对象和数组的通用forEach函数

function forEach(obj, fn) {
    var key;
    if (obj instanceof Array) {
        obj.forEach(function (item, index) {
            fn(index, item);
        })
    } else {
        for (key in obj) {
            fn(key, obj[key]);
        }
    }
}

var arr = [1, 2, 3];
forEach(arr, function (index, item) {
    console.log(index, item);
})

var obj = {x: 100, y: 200};
forEach(obj, function (key, value) {
    console.log(key, value);
})

5、pwa

PWA全称Progressive Web App,即渐进式WEB应用。

一个 PWA 应用首先是一个网页, 可以通过 Web 技术编写出一个网页应用。

随后添加上 App Manifest 和 Service Worker 来实现 PWA 的安装和离线等功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值