1. JSON
JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。
在项目开发中,使用 JSON 作为前后端数据交换的方式。
在前端通过将一个符合 JSON 格式的数据结构序列化为 JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。
在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,
- JSON.stringify 函数,将符合 JSON 格式的数据结构转换为一个 JSON 字符串
- JSON.parse() 函数,将 JSON 格式的字符串转换为一个 js 数据结构
JSON.parse(JSON.stringify(obj)) :可以实现对象的深拷贝
2. js的宏任务和微任务
2.1. 同步和异步的区别
- 同步指的是当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,那么这个进程会一直等待下去,直到消息返回为止再继续向下执行。
- 异步指的是当一个进程在执行某个请求时,如果这个请求需要等待一段时间才能返回,这个时候进程会继续往下执行,不会阻塞等待消息的返回,当消息返回时系统再通知进程进行处理。
2.2. 事件循环
js 是单线程运行的,在代码执行时,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行。
在执行同步代码时,如果遇到异步事件,js 引擎并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务。
当异步事件执行完毕后,再将异步事件对应的回调加入到一个任务队列中等待执行。
任务队列可以分为宏任务队列和微任务队列,当当前执行栈中的事件执行完毕后,js 引擎首先会判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。当微任务队列中的任务都执行完成后再去执行宏任务队列中的任务。
js中的事件被划分为宏任务和微任务
- 一开始整个脚本作为一个宏任务执行
- 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
- 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完
- 执行浏览器UI线程的渲染工作
- 检查是否有
Web Worker
任务,有则执行 - 执行完本轮的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空
微任务包括: MutationObserver
、Promise.then()或catch()
、Promise为基础开发的其它技术,比如fetch API
、V8
的垃圾回收过程、Node独有的process.nextTick
。
宏任务包括:script
、setTimeout
、setInterval
、setImmediate
、I/O
、UI rendering
。
3. 一些常见的字符串操作方法
var str = "Hello World!"
1.字符串拼接:concat方法
字符串拼接,可接收多个变量,一起拼接,不改变原字符串
var str1 = "javascript";
var res = str.concat(str1);
res= ''Hello World!javascript";
2.子字符串获取:substr(),substing(),slice()
这几个字符串都是基于原字符串创建新字符串的方法,均返回截取的那部分的字符串,不改变原字符串
- str.slice(start,end),截取str从start到end的所有字符(包含起始位置,不包含结束位置)
- str.substring(start,end) 截取str从start到end的所有字符(包含起始位置,但不包含结束位置)
- str.substr(start,length)截取str从start开始的length个字符(包含起始位置)
3.字符串位置:indexOf(),lastIndexOf()
str.indexOf("o");//从前往后找
str.lastIndexOf("o");//从后往前找
第二个参数表示从哪里开始找
返回子字符串的位置,没有则为-1
4.去除字符串前置和后缀的空格trim()
返回新的副本字符串,不改变原字符串
trimRight();trimLeft()
5.字符串大小写转换
toLowerCase();
toUpperCase()
不改变原字符串
6.字符串模式匹配
match()
var str = 'cat, hat, fat'
var pattern = /.at/
var res = str.match(pattern);
alert(res[0]) //cat
返回与规定模式匹配的字符串组成的数组
search()
返回匹配的字符串的索引
replace()
第一个参数可以是一个模式匹配也可以是一个字符串,第二个为替换的字符串
var str = 'cat, hat, fat'
var res = str.replace('at','ond')
alert(res)//'cond,hat,fat'
var res2 = str.replace(/at/g,'ond')
alert(res2)//'cond,hond,fond'
split()
将字符串根据指定的分隔符将字符串分割为多个子字符串,保存在一个数组里
var str = 'cat,hat,fat'
var res =str.split(',')
alert(res)//['cat','hat','fat']
var res2 = res.join("+")
alert(res2)//cat+hat+fat
与之相反的过程join()函数,第二个参数指定连接的字符串
4. 一些常见的数组操作方法
改变原数组的方法: push()、pop()、shift()、unshift()、splice()、reverse()、sort()、copyWithin()、fill()。
不改变原数组的方法: concat()、slice()、map()、filter()、reduce()、reduceRight()、join()、includes()、indexOf()、find()、findIndex()、every()、some()。
var arr= [1,2,3,4,5]
1.push(),pop(),unshift(), shift():去除第一个,
- 均会改变原数组
- push 和 unshift返回更改后的数组长度
- pop 和 shift返回删除的元素
2.indexOf()/find()/ findIndex()
- indexOf以具体元素为参数,find和findindex以函数为参数
- find返回元素,findIndex和indexOf返回索引;找不到时find返回undefined,findIndex 和indexOf返回-1。
3.toString()
4.join()
如果不传递参数,则默认为","分割将数组转换为字符串,不改变原数组
5.concat():拼接数组
返回新数组,不改变原数组
6.reverse()
arr.reverse()会改变原数组
7.slice(start,end)
截取原数组的一部分,返回新数组,不改变原数组,结束位置不包含end
8.splice():
删除原数组的一部分成员,并可以在被删除的位置添加入新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组。
splice(start,delNum,*addElement1, *** *addElement2,... *** )第一个参数是删除的起始位置,第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素。
var arr = ['a', 'b', 'c', 'd', 'e', 'f'];
arr.splice(4, 2) // ["e", "f"] 从原数组4号位置,删除了两个数组成员
console.log(arr) // ["a", "b", "c", "d"]
var arr = ['a', 'b', 'c', 'd', 'e', 'f'];
arr.splice(4, 2, 1, 2) // ["e", "f"] 原数组4号位置,删除了两个数组成员,又插入了两个新成员
console.log(arr) // ["a", "b", "c", "d", 1, 2]
var arr = ['a', 'b', 'c', 'd', 'e', 'f'];
arr.splice(-4, 2) // ["c", "d"] 起始位置如果是负数,就表示从倒数位置开始删除
var arr = [1, 1, 1];
arr.splice(1, 0, 2) // [] 如果只插入元素,第二个参数可以设为0
conlose.log(arr) // [1, 2, 1, 1]
var arr = [1, 2, 3, 4];
arr.splice(2) // [3, 4] 如果只有第一个参数,等同于将原数组在指定位置拆分成两个数组,返回后面的数组
console.log(arr) // [1, 2]
9.sort()
- 原数组会被改变
- sort方法不是按照大小排序,而是按照对应字符串的字典顺序排序ASICII。也就是说,数值会被先转成字符串,再按照字典顺序进行比较,所以101排在11的前面。
- 自定义方式排序:传入函数定义排序方式
可以传入一个函数作为参数,表示按照自定义方法进行排序。该函数本身又接受两个参数,表示进行比较的 两个元素。
升序排序return a-b,降序排序return b-a。
arr = [3,7,1]
function numberSort(a,b){
return a - b;//升序排列
return b - a;//降序排列
}
arr.sort(numberSort)
10.五个数组迭代方法:都不改变原数组,可以传入三个参数:item.index,array
map()、filter()、every()、forEach()、some()
11.两个迭代方法
reduce()
array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
第一个参数:中间结果,上一次return回来的中间结果值
第二个参数:当前拿到的项
第三个参数:下标值
很多例子: www.jianshu.com/p/e375ba1cf…
//例子:求平均数
let arr = [11,33,66]
let res = arr.reduce(function(tmp,item,index,array){
//当不是最后一次的时候,只算和
if(index != arr.length-1){
return tmp + item;
//最后算平均数
}else{
return (tmp + item)/arr.length;
}
})
reduceRight()
方向相反
4.1. 数组的二维定义
// 循环或直接赋值
const arr = [1,[2,3]]
// 循环
const arr = []
for(let i = 0;i < 3;i++) {
for ( let j = 0;j < 3;j++){
arr[i][j] = 0
}
}
// fill方法
const arr = Array(10).fill(0)
for (let i = 0;i < 10;i++){
arr[i] = Array(10).fill(0)
}
// Array.from
const arr = Array.from(Array(2),() => Array(10).fill(0))
// fill + map
const arr = Array(3).fill(0).map(x => Array(3).fill(0))
Array.from
语法:Array.from(类数组, 回调函数, 参数)
说明:
- 类数组:转换成数组的类似数组或可迭代的对象
- 回调函数:可选参数,如果加了这个参数,表示数组中的元素都要执行这个回调函数, 作用类似于数组的 map 方法,用来对每个元素进行处理,将处理后的值放入返回的数组
- 参数:可选,执行回调函数时的this对象
let a = Array.from(123456789);
let b = Array.from("123456789");
console.log(a); // []
console.log(b); // ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
// 加上回调函数
let a = Array.from([11,22,33,44,55,66,77,88,99],w => w * w * w);
console.log(a);
// [1331, 10648, 35937, 85184, 166375, 287496, 456533, 681472, 970299]
//生成一个数字序列。因为数组在每个位置都使用 `undefined` 初始化,下面的m值将是 `undefined`
let b = Array.from({length:5},(m,k) => k);
console.log(b);
// [0, 1, 2, 3, 4]
5. 类数组
一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有
- arguments
- DOM 方法的返回结果
- 函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数
- 字符串
想要遍历类数组需要将类数组转换为数组:
类数组转换为数组的方法有这样几种:
let arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 };
let arrayLike2 = document.getElementsByTagName('div');
// (1)通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
// (2)通过 call 调用数组的 splice 方法来实现转换,不适用于arrayLike2类型的,dom不能删除属性
Array.prototype.splice.call(arrayLike,0);
// (2)通过 call 调用数组的 concat 方法来实现转换, 不适用于arrayLike类型的,是对象,不能用于apply参数
Array.prototype.concat.apply([], arrayLike);
// (4)通过 Array.from 方法来实现转换
Array.from(arrayLike);
// (5) 使用扩展运算符,不适用于arrayLike类型的,是对象,需要用{...arrayLike}
[...arrayLike]
(6)使用for in 或for of迭代然后逐个加入新数组中
6. apply和call的理解,bind
call\apply\bind可以重新定义函数的执行环境, 动态改变this,,使得某个对象可以使用其他对象定义的方法
function.call(obj,param1,param2...)
**obj:**这个对象将替代Function类里的this对象
apply第二个参数是数组,call和bind后面可以传递多个参数
[关于JS中Apply和Call的一些理解]blog.csdn.net/u010979495/…
应用: apply会将一个数组转换为一个参数接一个参数的传递给方法:
因为Math.max 参数里面只支持Math.max(param1,param2,param3…),所以可以根据刚才apply的这一特点来解决
var max = Math.max.apply(null,array) === Math.max(param1,param2,param3…);
这样轻易的可以得到一个数组中最大的一项
apply、call、bind比较
var obj = {
x: 81,
};
var foo = {
getX: function() {
return this.x;
}
}
console.log(foo.getX.bind(obj)()); //81
console.log(foo.getX.call(obj)); //81
console.log(foo.getX.apply(obj)); //81
当你希望改变上下文环境之后并非立即执行,使用 bind() 方法。而 apply/call 则会立即执行函数。
总结:
- apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
- apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
- apply 、 call 、bind 三者都可以利用后续参数传参;
- bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。
7. 运算符号带来的隐式类型转换
var foo = "10"+3-"1";
console.log(foo); //102
对于“+”来说,有两个含义:第一个含义是做字符串拼接,第二个含义是加减法中的加法。
1,如果操作数里有一个是字符串,其他的值将被转换成字符串;
2,其他情况,操作数转换成数字执行加法运算。
而对于“ - ”来说,只有一个含义,就是做减法,自然不会转化成字符串了。
当使用!逻辑非运算符进行转化的时候,会尝试把数据转化成布尔值
console.log([] == 0) // true
console.log(![] == 0) // true
// [] == 0 --> [].valueOf().toString()得到空字符串,Number('') == 0 成立
// ![] == 0 --> Boolean([])得到true再取反,最后转化成数字0,Number(!true) == 0 成立
console.log([] == ![]) // true
console.log([] == []) // false
// [] == ![] --> [].valueOf().toString()得到空字符串,Number('')取得0,Boolean([])得到true再取反,转化成数字0,最后Number('') == Number(!true) 成立
// [] == [] --> 两个数组比较是因为两个数据的引用指向不一致,所以 [] == [] 不成立
console.log({} == !{}) // false
console.log({} == {}) // false
// {} == !{} --> {}.valueOf().toString()得到'[object Object]',Boolean({})得到true再取反,所以 '[object Object]' == false 不成立
// {} == {} --> 两个对象比较是因为两个数据的引用指向不一致,所以 {} == {} 不成立
8. this
this总是指向调用它的对象:,与在哪里定义没有关系,总是在运行时才确定,指向当前执行的上下文
- 定义对象时是不产生作用域的
- 构造函数中定义函数,该函数的上级作用域是构造函数
几种绑定方式:
优先级:new关键字 > 显式绑定 > 隐式绑定 > 默认绑定
1、 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。 (默认调用)
2、对象调用模式: 函数作为对象的方法被调用,this指向调用者 (隐式调用)
3、构造器调用模式: 用于构造函数,this指向新对象 (new绑定)
var test = function(){
alert(this === window);
}
new test();
//结果为fasle,使用new则是构造函数,构造函数创建了一个新的空的对象,
// 然后再有这个空的对象指向函数test的代码,这时this指向的是这个新的对象
3、ES6的箭头函数,箭头函数不会创造块级作用域,无法生成一个独立的环境,this指向上层的this,箭头函数使用call、apply、bind无效
4、函数被apply/call/bind等调用则指向对应的那个对象 (显式调用), 当显示绑定的值为 null/undefined 时,this直接绑定window
5、嵌套函数的this值:会在非严格模式下指向window,严格模式下指向undefined
var a = {
b : function(){
var func = function(){
console.log(this.c);
console.log(this);
}
func();
},
c : 'Hello!'
d:function(){
console.log(this.c)
}
}
a.b();//undefined
//这里的this指向window,这里的嵌套函数,this为window,可以这样理解,a.b()仍然是一个完整的函数,此时这个函数是window调用的,
a.d();//Hello
//这里的d。已经被a调用了,所以this指向a
//非严格模式下
var obj = {
test:function (){
var self = this;
console.log(this === obj); //true
f();
function f(){
console.log(this === obj); //false
console.log(self === obj); //true
console.log(this === window); //true
};
}
};
obj.test();
//严格模式下
"use strict"
var obj = {
test:function (){
var self = this;
console.log(this === obj); //true
f();
function f(){
console.log(this === obj); //false
console.log(self === obj); //true
console.log(this === window); //false
console.log(this === undefined); //true
};
}
};
obj.test();
解决方法:
1)在进入嵌套函数之前,用一个变量保存this
var obj = {
name: 'Jane',
friends: [ 'Tarzan', 'Cheeta' ],
loop: function () {
'use strict';
var that = this ;
this.friends.forEach(
function (friend) {
console.log(that.name+' knows '+friend);
}
);
}
};
obj.loop();//这样可以正常输出
2)使用bind
var obj = {
name: 'Jane',
friends: [ 'Tarzan', 'Cheeta' ],
loop: function () {
'use strict';
this.friends.forEach(
function (friend) {
console.log(this.name+' knows '+friend);
}.bind(this)
);
}
};
obj.loop();//这样可以正常输出
3. 使用forEach的callback
var obj = {
name: 'Jane',
friends: ['Tarzan', 'Cheeta'],
loop: function() {
'use strict';
this.friends.forEach(function(friend) {
console.log(this.name + ' knows ' + friend);
}, this);
}
};
obj.loop();
4. 使用箭头函数:这样就会使用上一层的this
var obj = {
name: 'Jane',
friends: ['Tarzan', 'Cheeta'],
loop: function () {
'use strict';
this.friends.forEach(
(friend) => {
console.log(this.name + ' knows ' + friend);
}
);
}
};
obj.loop();//这样可以正常输出
9. 全局变量和局部变量
var a,b;
(function(){
alert(a); //这是第一个输出的,先在局部没找到a变量,然后去全局找,找到了但没定义,输出 undefined
alert(b); //这是第二个输出的,其他同上
var a=b=3; //定义一个局部变量a=3,然后给全局变量b赋值 b=3;相当于 var a = 3;b = 3;
alert(a); //这是第三个输出,局部变量a=3
alert(b); //这是第四个输出,全局变量b=3
})(); //这个函数体已经执行完毕,里面的内存已经被垃圾回收器回收,局部变量a销毁
alert(a); //这是第五个输出,全局变量a=undefined
alert(b); //这是第六个输出,全局变量b=3
10. 检测数据类型
10.1. js数据类型
基本数据类型(Primitive Data Types)
- Number:用于表示数字,可以是整数或浮点数。例如:let num = 42; let pi = 3.14;。
(-(2^53 - 1) 到 即 2^53 - 1)
-
- MIN_SAFE_INTEGER: 表示在 JavaScript中最小的安全的 integer 型数字 (
-(253 - 1)
)。 - MAX_SAFE_INTEGER: 表示在 JavaScript 中最大的安全整数(
253 - 1
)。 - Infinity > Number.MAX_SAFE_INTEGER; (-Infinity 无穷大和无穷小)
- MIN_SAFE_INTEGER: 表示在 JavaScript中最小的安全的 integer 型数字 (
- String:表示文本数据,用单引号或双引号括起来。例如:let str = 'Hello, world!';。
- Boolean:只有两个值,true和false,用于表示逻辑状态。例如:let isTrue = true;。
- Null:表示一个空值,通常用于初始化变量或表示对象不存在。例如:let emptyValue = null;。
- Undefined:表示一个未初始化的变量或者没有返回值的函数的结果。
- Symbol(ES6 引入):一种唯一的、不可变的数据类型,通常用于作为对象属性的标识符。例如:let sym = Symbol('description');。 obj[sym] = 'aa'
- BigInt是一种无符号整数类型,可以存储任意长度的整数。当需要将BigInt与普通数字(Number类型)进行交互时,需要进行显式的类型转换。否则会报错。
const bigInt1 = BigInt("12345678901234567890"); // 使用字符串创建 BigInt
const bigInt2 = 12345678901234567890n; // 使用数字字面量后缀 n 创建 BigInt
// 运算
onst regularNumber = 123;
const bigNumber = BigInt(123);
// 错误的用法,会抛出 TypeError
// const result = regularNumber + bigNumber;
//Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions at <anonymous>:1:30
// 正确的用法,显式转换类型
const result = BigInt(regularNumber) + bigNumber;
//console.log(result)
// 246n
引用数据类型(Reference Data Types)
- Object:可以存储多个键值对,键可以是字符串或 Symbol、数字、变量,值可以是任何数据类型。例如:let obj = { key: 'value' };。
const dynamicKey = 'key_' + Date.now();
const obj = {
[dynamicKey]: "动态计算的键名"
};
- Array:用于存储一组有序的数据,可以包含不同类型的数据。例如:let arr = [1, 'two', true];。
- Function:在 JavaScript 中,函数也是一种对象,可以作为变量传递、作为参数传递给其他函数或从函数中返回。例如:function add(a, b) { return a + b; }。
- Date:用于处理日期和时间。例如:let now = new Date();。
- RegExp:正则表达式对象,用于匹配文本中的模式。例如:let regex = /\d+/;。
- Map(ES6 引入):键值对的集合,其中键和值可以是任何类型。例如:let myMap = new Map(); myMap.set('key', 'value');。
- Set(ES6 引入):一组唯一值的集合。例如:let mySet = new Set(); mySet.add(1);。
10.2. 检测数据类型
(1)typeof作为检测数据类型的关键字,返回的是字符串:对于引用类型返回的都是object
console.log(typeof 4);//number
console.log(typeof 'str');//string
console.log(typeof true);//boolean
console.log(typeof undefined);//undefined
console.log(typeof null);//object
console.log(typeof [1,2,3]);//object
console.log(typeof {});//object
console.log(typeof function(){});//function
console.log(typeof /a/);//object(有的浏览器将正则表达式返回的是function)
// 特殊的
typeof NaN; // "number"
(2)instanceof关键字,XX instanceof YY表示XX是 YY的实例,判断具体的引用类型,这里注意instanceof只能用于引用类型,返回布尔值
instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
console.log([] instanceof Array);//true
console.log({} instanceof Object);//true
console.log(1 instanceof Number);//false,不能判断基本类型
(3) constructor
通过constructor指向其对应的构造函数判断类型(如果创建一个对象来改变它的原型,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
(4)Object.prototype.toString; 使用 Object 对象的原型方法 toString 来判断数据类型:
var a = Object.prototype.toString;
console.log(a.call(2)); // [object Number]
console.log(a.call(true)); // [object Boolean]
console.log(a.call('str')); // [object String]
console.log(a.call([])); // [object Array]
console.log(a.call(function(){})); // [object Function]
// 判断是否是带有async开头的异步函数
console.log(a.call(async function(){})); // [object AsyncFunction]
console.log(a.call({})); // [object Object]
console.log(a.call(undefined)); // [object Undefined]
console.log(a.call(null)); [object Null]
// 注意,ARRAY等类型会重写tostring方法,所以直接使用会不对,需要使用原型上的方法
const obj = [3,4]
console.log(Object.prototype.toString.call(obj)) // [object Array]
console.log(obj.toString()) // 3,4
总结
方法 基础数据类型 引用类型 注意事项
typeof √ × 简单易用,但对某些特殊值判断不准确。
instanceof × √ 适用于判断对象的具体类型,但对基本数据类型不适用。
Object.prototype.toString.call() √ √ 最全面的判断方法,适用于各种类型。
constructor √ 部分 √ 可以处理基本数据类型,但在继承情况下需要注意调整指向。
严格对等 === √ 部分 √ 主要用于简单的条件判断,但对于复杂的数据类型判断不够全面。
10.3. 数据类型转换
Number() 函数在JavaScript中用于将不同类型的参数转换为数字。 它既可以作为函数使用,也可以作为构造器使用
运算时调用Number方法之后再运算
let num1 = Number("123"); // 123
let num2 = Number(true); // 1
let num3 = Number(null); // 0
let num4 = Number(undefined); // NaN
let num5 = Number(""); // 0
let num6 = Number("not a number"); // NaN
11. js判断数组的方法
- instanceOf方法
var arr = [1,2,3];
console.log(arr instanceOf Array)//true
- constructor方法
console.log(arr.constructor == Array)//true
- Object.prototype.toString.call()方法
console.log(Object.prototype.toString.call(arr))//[object Array]
- Array.isArray()
console.log(Array.isArray(arr))//true
12. 构造函数、实例对象和继承
12.1. 构造函数
构造函数也是一个普通函数,创建方式和普通函数一样,但构造函数习惯上首字母大写
2、构造函数和普通函数的区别在于:调用方式不一样。作用也不一样(构造函数用来新建实例对象)
3、调用方式不一样。
a. 普通函数的调用方式:直接调用 person();
b.构造函数的调用方式:需要使用new关键字来调用 new Person();
4、构造函数的函数名与类名相同:Person( ) 这个构造函数,Person 既是函数名,也是这个对象的类名
5、内部用this 来构造属性和方法
function Person(name,job,age){
this.name=name;
this.job=job;
this.age=age;
this.sayHi = function(){
alert("Hi")
}
}
6、构造函数的return值
构造函数里没有显式调用return时,默认是返回this对象,也就是新创建的实例对象。
当构造函数里调用return时,分两种情况:
a. return的是五种简单数据类型:String,Number,Boolean,Null,Undefined。
这种情况下,忽视return值,依然返回this对象。
b. return的是Object
这种情况下,不再返回this对象,而是返回return语句的返回值。
7、new操作符干了什么
1)先创建了一个新的空对象
2)将构造函数的作用域传给新对象,(这时的this就指向新对象)
3)执行构造函数中的内容,为这个新对象添加属性
4)返回新对象
12.2. 原型链和继承
构造函数.prototype = 原型对象
原型对象.constructor = 构造函数
实例.constructor = 构造函数 // 顺着原型链中读出来的
实例.__proto__ = 原型对象
prototype: 显式原型
__ proto__: 隐式原型
原型链继承的缺点:原型对象上引用类型值会受到实例的影响而发生修改
构造函数继承的缺点:
- 每个实例都拷贝一份构造函数的所有方法和属性,函数无法复用
2. 方法都作为了实例自己的方法,当需求改变,要改动其中的一个方法时,之前所有的实例,他们的该方法都不能及时作出更新。只有后面的实例才能访问到新方法。
组合继承
//父类:人
function Person () {
this.head = '脑袋瓜子';
this.emotion = ['喜', '怒', '哀', '乐']; //人都有喜怒哀乐
}
//将 Person 类中需共享的方法放到 prototype 中,实现复用
Person.prototype.eat = function () {
console.log('吃吃喝喝');
}
Person.prototype.sleep = function () {
console.log('睡觉');
}
Person.prototype.run = function () {
console.log('快跑');
}
//子类:学生,继承了“人”这个类
function Student(studentID) {
this.studentID = studentID;
// 使用call方法可以让当前student这个构造函数构造出自己的属性,即实例上的隔离属性
Person.call(this);
}
Student.prototype = new Person(); //此时 Student.prototype 中的 constructor 被重写了,会导致 stu1.constructor === Person
Student.prototype.constructor = Student; //将 Student 原型对象的 constructor 指针重新指向 Student 本身
var stu1 = new Student(1001);
console.log(stu1.emotion); //['喜', '怒', '哀', '乐']
stu1.emotion.push('愁');
console.log(stu1.emotion); //["喜", "怒", "哀", "乐", "愁"]
var stu2 = new Student(1002);
console.log(stu2.emotion); //["喜", "怒", "哀", "乐"]
stu1.eat(); //吃吃喝喝
stu2.run(); //快跑
console.log(stu1.constructor); //Student
其他点
- 原型链的终点是 null
- 获取实例的原型 Object.getPrototypeOf
var F = function () {}; // var F = new Function()
var f = new F();
Object.getPrototypeOf(f) === F.prototype // true
几种特殊对象的方法:
// 空对象的原型是 Object.prototype
Object.getPrototypeOf({}) === Object.prototype // true
// Object.prototype 的原型是 null
Object.getPrototypeOf(Object.prototype) === null // true
// 函数的原型是 Function.prototype
function fun() {}
Object.getPrototypeOf(fun) === Function.prototype // true
- 判单属性在实例上还是原型上: hasOwnProperty
function iterate(obj){
var res=[];
for(var key in obj){ // 获取所有属性
if(obj.hasOwnProperty(key))
res.push(key+': '+obj[key]);
}
return res;
}
13. 作用域
13.1. 作用域
(1)全局作用域
- 最外层函数和最外层函数外面定义的变量拥有全局作用域
- 所有未定义直接赋值的变量自动声明为全局作用域
- 所有window对象的属性拥有全局作用域
- 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。
(2)函数作用域
- 函数作用域声明在函数内部的变量,一般只有固定的代码片段可以访问到
- 作用域是分层的,内层作用域可以访问外层作用域,反之不行
(3)块级作用域
- 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由
{ }
包裹的代码片段) - let和const声明的变量不会有变量提升,也不可以重复声明
- 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。
cloud.tencent.com/developer/a…
JS中的函数运行在他们被定义的作用域里,而不是被执行的作用域里。
var name = "test"
function callMePlz() { // 他是在全局作用域中声明的
console.log(name);
}
function myFunc() {
var name = "endlesscode";
callMePlz();
}
myFunc();//test
13.2. 闭包
定义:闭包是指有权访问另一个函数作用域中的变量的函数
js的作用域链:可以访问函数A内部定义的变量的,有A本身,还有A内部定义的函数B,如果想要让A外部的函数C也访问到这个A内部的变量,可以让A内部的B作为返回值将他返回,再赋值给C,这样C就可以操作B,进而可以操作A内部的变量了。此时这个变量将和C同在,即使A已经执行完毕,这个内部变量也不会被js的自动回收机制回收掉。
闭包真正的含义是,如果一个函数访问了此函数的父级及父级以上的作用域变量,就可以称这个函数是一个闭包。
这样就实现了在函数外部访问函数内部变量的操作。
闭包的两个最大的用处:一个是可以读取到函数内部的变量,另一个就是让这些变量的值始终保持在内存中,第二个作用可以让函数多次调用的时候,变量会累加
13.3. 匿名函数
//正常函数的形式:test是函数的名字
function test(){
alert("test");
}
//去掉名字,报错,匿名函数不能单独存在
function(){
alert("test")
}
//加上括号,可以省略,函数不会执行
(function(){
alert("test")
})
//再加上括号,匿名函数立即执行//通过表达式自我执行
(function(){
alert("test")
})()
//参数放在后面的括号里
(function(str){
alert("test"+str)
})("托尔斯泰")
匿名函数的作用:
1、通过匿名函数可以实现闭包,关于闭包在后面的文章中会重点讲解。在这里简单介绍一下:闭包是可以访问在函数作用域内定义的变量的函数。若要创建一个闭包,往往都需要用到匿名函数。
2、模拟块级作用域,减少全局变量。执行完匿名函数,存储在内存中相对应的变量会被销毁,从而节省内存。再者,在大型多人开发的项目中,使用块级作用域,会大大降低命名冲突的问题,从而避免产生灾难性的后果。自此开发者再也不必担心搞乱全局作用域了。
14. js的组成
1)ECMAScript(核心)
规定了语言的组成部分,具体包括语法、类型、语言、关键字、保留字、操作符、对象。对该标准规定了各 个方面内容的语言的描述。
2)DOM(文档对象模型)
DOM把整个页面映射为一个多层次节点结构。HTML或者XML页面中的每个组成部分都是某种类型的节点,这些节点又包含着不同类型的数据。
3)BOM(浏览器对象模型)
BOM处理浏览器窗口和框架, 它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的方法和接口。window 对象含有 location 对象、navigator 对象、screen 对象、document等子对象。
15. js严格模式
’use strict'声明整个文件都是在严格模式下执行,代码更加工整规范
- 变量必须显式声明
- 对象中有重复的属性会报错
- 函数有重名的参数会报错
- call/apply/bind的第一个参数为null/undefined时,this为null/undefined,否则为window
16. 变量提升,this一道很经典的题
变量提升的表现是,无论在函数中何处位置声明的变量,好像都被提升到了函数的首部,可以在变量声明前访问到而不会报错。
变量提升的作用:
- 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
- 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行
全局预编译:先编译,再执行
- 编译:先查找变量声明,作为当前全局对象(GO)的属性,值为undefined
- 编译:再查找函数声明,作为当前全局对象(GO)的属性,值为对应fn
- 执行:从上到下依次执行
- 最终结果导致编译结束时函数声明会覆盖变量声明,但执行时变量如果有赋值语句则再次生效会覆盖前面的函数声明。那么读取的地方如果位于赋值之前,则为函数内容,如果在赋值之后,则为变量内容。参考B站 杰哥课堂 www.bilibili.com/video/BV1C5…
function Foo() {
getName = function () { console.log(1); };
return this;
}
Foo.getName = function () { console.log(2); };
Foo.prototype.getName = function () { console.log(3); };
var getName = function () { console.log(4); };
function getName() { console.log(5); }
//问:出以下输出结果
Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 1
getName(); // 1
解析:
Foo.getName();
运行的是function () { console.log(2); };
,会先查找构造函数本身属性,若未找到再原型链上查找,因此输出2
getName();
var getName = function () { console.log(4); };
function getName() { console.log(5); }
// 存在函数提升和变量提升,先变量提升,再函数提升,函数提升会覆盖变量提升
// 上述两句代码真正的执行顺序如下`
var getName = undefined; // 代码解析时先变量提升
function getName() { console.log(5); } // 代码解析时再函数提升覆盖
getName = function () { console.log(4); }; // 代码执行时进行覆盖
所以最终getName被function () { console.log(4); };
覆盖了,输出4
Foo().getName();
首先Foo()执行的返回值是this,所以这个可以看做是this.getName(),此时this指向全局
全局范围内的this.getName就是getName = function () { console.log(4); };
但是Foo()中的getName = function () { console.log(1); };
没有对getName使用let声明,所以此时全局的getName被Foo中的function () { console.log(1); };
覆盖了,所以输出1
getName();
这个getName和this.getName是同一个变量,因此输出和Foo().getName();
一样,都为1
函数预编译:
只要声明了局部函数, 函数的优先级最高 没有声明局部函数, 实参的优先级高
整体来说: 局部函数 > 实参 > 形参和局部变量
阶段 | 行为 |
---|---|
预编译阶段 | 1. 形参初始化 2. 函数声明提升(覆盖形参) 3. 变量声明初始化(不覆盖同名函数) |
执行阶段 | 1. 按代码顺序执行赋值和调用 2. 函数声明不再处理 |
function a(b, c) {
console.log(b)
var b = 0
console.log(b)
function b() {
console.log(222)
}
console.log(c)
}
a(1)
// 输出
ƒ b() {
console.log(222)
}
0
undefined
undefined
17. 节流和防抖
17.1. 函数防抖(debounce):
当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。如下图,持续触发scroll事件时,并不执行handle函数,当1000毫秒内没有触发scroll事件时,才会延时触发scroll事件。
const debounce = (fn, delay) => {
let timer = null;
return function () {
const ctx = this;
const args = arguments;
if (timer) clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(ctx, args)
}, delay);
}
}
function handle(){
console.log(Math.random())
}
document.getElementById("btnClick").addEventListener("click",debounce(handle,1000))
(1)刚一进来这个函数,debounce就已经开始执行,进行到var TimeOut = null;然后用return函数将下面的操作赋给“click",接下来每次点击唤醒click,那么执行的就是return后面的内容,
(2)第一次timeOut为null,执行第一个setTimeout,给TimeOut一个标记值,接下来在delay时间内,下一次click又来了,此时TimeOut不为null,(是上一次setTimeout的标记值),那么则清除这个计时器,进行TimeOut = setTimeout(func,wait);下一次又来了,此时TimeOut不为null,(是上一次setTimeout的标记值)
(3)如果超过了delay时间,下一次点击没有来,则这个计时器不会被清除,开始执行handle的函数内容
17.2. 函数节流(throttle):
当持续触发事件时,保证一定时间段内只调用一次事件处理函数。节流通俗解释就比如我们水龙头放水,阀门一打开,水哗哗的往下流,秉着勤俭节约的优良传统美德,我们要把水龙头关小点,最好是如我们心意按照一定规律在某个时间间隔内一滴一滴的往下滴。如下图,持续触发scroll事件时,并不立即执行handle函数,每隔1000毫秒才会执行一次handle函数。
方法一:使用时间戳
function throttle(func,delay){
var pre = Date.now();
return function(){
var content = this;
var args = arguments;
var now = Date.now();
if(now - pre >= delay){
func.apply(content,args);
pre = Date.now();
}
}
}
function handle(){
console.log(Date.now())
}
document.getElementById("btnClick").addEventListener("click",throttle(handle,1000))
当高频事件触发时,第一次会立即执行(给scroll事件绑定函数与真正触发事件的间隔一般大于delay,如果你非要在网页加载1000毫秒以内就去滚动网页的话,我也没办法o(╥﹏╥)o),而后再怎么频繁地触发事件,也都是每delay时间才执行一次。而当最后一次事件触发完毕后,事件也不会再被执行了 (最后一次触发事件与倒数第二次触发事件的间隔小于delay,为什么小于呢?因为大于就不叫高频了呀(╹▽╹))。
方法二:使用定时器:
// 节流throttle代码(定时器):
var throttle = function(func, delay) {
var timer = null;
return function() {
var context = this;
var args = arguments;
if (!timer) {
timer = setTimeout(function() {
func.apply(context, args);
timer = null;
}, delay);
}
}
}
function handle() {
console.log(Math.random());
}
document.getElementById("btnClick").addEventListener("click",throttle(handle,1000))
当触发事件的时候,我们设置一个定时器,再次触发事件的时候,如果定时器存在,就不执行,直到delay时间后,定时器执行执行函数,并且清空定时器,这样就可以设置下个定时器。当第一次触发事件时,不会立即执行函数,而是在delay秒后才执行。而后再怎么频繁触发事件,也都是每delay时间才执行一次。当最后一次停止触发后,由于定时器的delay延迟,可能还会执行一次函数。
两种方法结合:
// 节流throttle代码(时间戳+定时器):
var throttle = function(func, delay) {
var timer = null;
var startTime = Date.now();
return function() {
var curTime = Date.now();
var remaining = delay - (curTime - startTime);
var context = this;
var args = arguments;
clearTimeout(timer);
if (remaining <= 0) {
func.apply(context, args);
startTime = Date.now();
} else {
timer = setTimeout(func, remaining);
}
}
}
function handle(){
console.log(Date.now())
}
document.getElementById("btnClick").addEventListener("click",throttle(handle,1000))
在节流函数内部使用开始时间startTime、当前时间curTime与delay来计算剩余时间remaining,当remaining<=0时表示该执行事件处理函数了(保证了第一次触发事件就能立即执行事件处理函数和每隔delay时间执行一次事件处理函数)。如果还没到时间的话就设定在remaining时间后再触发 (保证了最后一次触发事件后还能再执行一次事件处理函数)。当然在remaining这段时间中如果又一次触发事件,那么会取消当前的计时器,并重新计算一个remaining来判断当前状态。
17.3. 总结
函数防抖:将几次操作合并为一此操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。
函数节流:使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。
区别: 函数节流不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数,而函数防抖只是在最后一次事件后才触发一次函数。 比如在页面的无限加载场景下,我们需要用户在滚动页面时,每隔一段时间发一次 Ajax 请求,而不是在用户停下滚动页面操作时才去请求数据。这样的场景,就适合用节流技术来实现。
18. 垃圾清除
18.1. 回收机制:
- Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
- JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续到页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。
- 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。
垃圾清理实现策略一:标记清除(常用)
当变量进入环境时,如在函数中var一个变量,此时将这个变量标记为进入环境,当变量离开环境的时候,则将其标记为离开环境,可以通过翻转某一个位来标记一个变量何时进入了环境。但标记不是重点,重点是标记了之后怎么来将其处理。垃圾收集器会在运行的时候给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量以及被环境中的变量应用的标记,在此之后再把加上标记的变量都将被视为准备删除的变量。最后,垃圾收集器完成内存的清楚工作,销毁那些带标记的值并收回它们所占用的内存空间
垃圾清理实现策略二:引用计数(不常用)
跟踪记录每个值被引用的次数,当这个值的引用次数变成0的时候,说明没有办法再访问这个这个值,就将其占用的内存空间收回来,下次再运行垃圾收集器的时候,就会释放哪些引用次数为0的值所占用的内存了。
但有循环引用问题如下
objectA 和 objectB 通过各自的属性相互引用,意味着它们的引用数都是2,在引用计数模式下不会被回收。在标记清理策略下,objectA 和 objectB 虽然互相引用了对方,但没有其他引用指向这两个对象,它们将会被标记为不可达,并最终会被垃圾回收器清除。
18.2. 减少垃圾回收
虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。
- 对数组进行优化: 在清空一个数组时,最简单的方法就是给其赋值为[ ],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。
- 对
object
进行优化: 对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。 - 对函数进行优化: 在循环中的函数表达式,如果可以复用,尽量放在函数的外面。
18.3. 内存泄漏
以下四种情况会造成内存的泄漏:
- 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
- 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。
19. 事件监听,写一个eventEmitter类,包括on()、off()、once()、emit()方法
1)、on(event,fn):注册事件监听,监听event事件,事件触发时调用fn函数;
2)、once(event,fn):为指定事件注册一个单次监听器,单次监听器最多只触发一次,触发后立即解除监听器;
3)、emit(event,arg1,arg2,arg3...):触发event事件,并把参数arg1,arg2,arg3....传给事件处理函数;
4)、off(event,fn):停止监听某个事件。
class EventEmitter{
constructor(){
this.handlers = {}
};
on(event,fn){
if(!this.handlers){
this.handlers = {}
}
if(!this.handlers[event]){
this.handlers[event] = []
}
this.handlers[event].push(fn);
};
emit(event,...args){
if(this.handlers[event]){
for(var i =0;i<this.handlers[event].length;i++){
this.handlers[event][i](...args);
}
}
};
off(event,fn){
if(!this.handlers[event]){
return;
}
//如果未指定要停止的事件,则停止监听整个事件
if(!fn){
delete this.handlers[event];
return;
}
//否则停止这个事件的某个回调函数的处理
var index = this.handlers[event].indexOf(fn);
this.handlers[event].splice(index,1);
};
once(event,fn){
var _fn = (...args)=>{
fn.apply(this,...args);
this.off(event,_fn);
}
this.on(event,_fn);
}
}
const event = new EventEmitter;
//事件注册,on,开始监听someEvent事件,事件触发时调用函数。
event.on("someEvent",(...args)=>{
console.log(...args);
})
//事件触发,并把参数arg1,arg2,arg3....传给事件处理函数;参数不固定
event.emit("someEvent",["abc","d","123"]);
//停止监听某个事件,或者不是停止监听某个事件,而是停止这个事件触发之后调用的某个函数
event.off("someEvent",(...args)=>{
console.log(...args);
});
//事件可以多次触发
event.emit("someEvent",["abc","d","123"]);
//未注册不能触发
event.emit("someEvent1",["abc1","d","123"]);
event.emit("someEvent2",["abc2","d","123"]);
//单次事件,只能触发一次
event.once("someEvent3",(...args)=>{
console.log(...args)
});
event.emit("someEvent3",["abc3","d","123"]);
//第二次就会失效
event.emit("someEvent3",["abc3","d","123"]);
20. ....args
1)不定参数
var myfn = function(...arg){console.log(arg)},对于这里的...arg是传入不定个参数,在函数内部的arg是个数组
function add(...x){
return x.reduce((m,n)=>m+n);
}
console.log(add(1,2,3));//6
console.log(add(1,2,3,4));//10
2). 拓展参数
在这里将数组转换成参数传入,它能够映射到每个单独的参数
var arg = [10,12,34]
var myfn = function(x,y,z){
console.log(x+y+z)
} ;
myfn(...arg)//56
//之前的方法,需要用apply
myfn.apply(null,arg);
21. for of 和for in
- for in 循环对象或数组,值为对象的属性或数组的下标,会包括原型链中的属性
const arr = [1,2,3]
const obj = {
a: 'wan',
b: 'sss'
}
for(let key in arr){
console.log(key) // 0,1,2
}
for(let key in obj){
console.log(key) // a,b
}
for of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构,可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。不能直接遍历 对象
const arr = [1,2,3]
const obj = {
a: 'wan',
b: 'sss'
}
for(let item of arr){
console.log(key) // 1,2,3
}
for(let item of obj){
console.log(item) // Uncaught TypeError: obj is not iterable
}
for of遍历对象
- 如果需要遍历的对象是类数组对象,用Array.from转成数组即可。
var obj = {
0:'one',
1:'two',
length: 2
};
obj = Array.from(obj);
for(var k of obj){
console.log(k)
}
- 如果不是类数组对象,就给对象添加一个[Symbol.iterator]属性,并指向一个迭代器即可。
22. js类和对象
js里面没有类(ES6以前)的语法,所以类的概念就通过创造一个对象来实现,这个对象可以实现多次实例化
实例都是对象,对象不一定是实例(有可能是构造函数)
如何判断一个对象是否属于一个类?
1、instanceof 运算符
判断构造函数的prototype是否在当前对象的原型链上
class MyClass {}
const obj = new MyClass();
console.log(obj instanceof MyClass); // true
2. constructor属性
console.log(obj.constructor === MyClass); // true
3.
console.log(Object.prototype.toString.call(obj) === "[object MyClass]");
23. 函数
(1)关于函数对象和实例对象
//函数:普通函数、函数对象、类
//函数
function Fn(){
}
//只有new操作之后,Person才是一个构造函数,fn是一个实例对象,简称为对象
var fn = new Fn();
//括号的旁边是函数,“."的旁边是对象,调用属性和方法
//Fn是一个函数,当作为对象使用的时候,就称为函数对象
console.log(Fn.prototype)
(2)回调函数的分类
- 同步回调函数:立即执行,完全执行完了才结束,不会放到队列中
const arr = [1,2,3,4]
//item=>{console.log(item)} 就是一个回调函数,遍历回调
arr.forEach(item=>{
console.log(item)
})
- 异步回调函数不会立即执行,会放到队列中将来执行定时器回调、ajax回调、Promise的成功失败的回调
setTimeout(()=>{
console.log('test1')
},0)
console.log(test2)
//打印出来test2 然后 test1
24. js的常见错误
(1)常见的内置错误类型
- ReferenceError:引用的变量不存在test.html:10 Uncaught ReferenceError: b is not defined
console.log(a)
- TypeError:数据类型不正确的错误Uncaught TypeError: Cannot read property 'xxxx' of null
let b = null;
console.log(b.xxxx)
- RangeError:数据值不在其允许的范围内多次嵌套,超过次数限制:Uncaught RangeError: Maximum call stack size exceeded
function fn(){
fn()
}
fn()
- SyntaxError:语法错误
const c = '""
(2)错误捕获
try catch
try {
//运行代码
} catch(err) {
//处理错误
}
throw抛出异常
var num = "a";
try {
if(num==" ") throw "值为空"; //刚才这个地方的分号写成中文状态下的分号,结果出出错了
if(isNaN(num)) throw "不是数字";
}
catch(err){
alert("错误信息:"+err);
}
25. Promise
(1)Promise是什么?
Promise解决异步编程的新的解决方案,防止出现回调地狱
Promise是一个构造函数,用来封装一个异步操作并且可以获取其结果
(2)三种状态:
pedding:等待过程,正在进行网络请求,或者定时器没有到时间
Fulfilled::满足状态,成功,回调resolve,回调.then
rejected:不满足状态,失败,调用reject,回调.catch
只能从pedding->Fulfilled,pedding->rejected
状态只能改变一次:结果数据:成功:value 失败:reason
(3)作用
主要用于异步计算:,
可以将多个异步操作队列化,按照期望的顺序执行,返回符合预期的结果
可以在对象之间传递和操作promise,帮助我们处理队列
(4)优点
- 执行回调函数的方式更加灵活:旧的:必须在启动异步任务之前指定回调函数Promise:启动异步任务=> 返回promise对象=>给promise对象绑定回调函数(指定一个时间之后再执行回调函数)
- 支持链式调用,解决回调地狱问题。回调地狱:多个异步任务嵌套串行进行,上一个任务的结果为下一个任务的条件,难以阅读,异常处理比较麻烦
- 链式编程 :.then()返回一个新的Promise实例,每个promise对应一个异步任务,所以它可以链式调用,异常传透
使用setTimeout模拟网络请求异步操作
(5)Promise构造函数:Promise(excutor){}
excutor函数:同步执行(resolve,reject)=>{}
resolve函数:内部定义成功时我们调用的函数 value=>{}, 其中参数是当前Promise对象的结果
reject函数:内部定义失败时我们调用的函数 reason=>{}
说明:excutor 会在Promise内部立即同步回调,异步操作在执行器中执行
(6)Promise 的使用方法
resolve是接口,then是方法的实现
then 返回的也是一个promise实例,状态是pedding,这样可以继续then
在then方法中,通过return将返回的promise实例的状态改为fullfilled
const p = new Promise((resolve,reject) => {
resolve('123')
})
// then 是promise 原型中的方法,有两个参数,第一个成功时回调,第二个失败时回调
p.then((res) => {
},(reason) => {
})
new Promise((resolve,reject)=>{
//1.第一次网络请求代码
setTimeout((data1)=>{
//下面两个只能执行一个,状态只能改变一次
resolve(data1)
reject(error)
},1000)
}).then((data1)=>{
//第一次处理的代码
console.log('onResolved()',data1);
// 把promise返回回去,后面才能继续调用then
return new Promise((resolve,reject)=>{
//2.第二次网络请求代码
setTimeout((data2)=>{
resolve(data2)
},1000)
})
}).then((data2)=>{
//第二次处理的代码
console.log(data2);
return new Promise((resolve,reject)=>{
//3.第三次网络请求代码
setTimeout((data3)=>{
resolve(data3)
},1000)
})
}).then((data3)=>{
//第三次处理的代码
console.log(data3);
},(err)=>{
console.log(err)
})
//then也可以传递两个参数,都是函数,第一个是成功时调用,第二个是失败时调用
语法糖模式:简写
const p1 = new Promise((resolve,reject)=>{
resolve(1) // 说明成功,参数传递
})
const p2 = Promise.resolve(2);
const p3 = Promise.reject(3);
p1.then(value=>{
console.log(value);
})
p3.catch(reason=>{
console.log('reason')
})
如果只是网络请求结果的多次使用封装,可以进行简写
new Promise((resolve,reject)=>{
//1.第一次网络请求代码
setTimeout(('aaa')=>{
resolve('aaa')
},1000)
}).then((data1)=>{
//第一次处理的代码
console.log(data1,第一次处理的代码);
return data1+'111'
}).then((data2)=>{
//第二次处理的代码
console.log(data2,第二次处理的代码);
return data2+'222'
}).then((data3)=>{
//第三次处理的代码
console.log(data3,第三次处理的代码);
}
(6)promise的all和race的区别;
某一个需求依赖两个请求,当这两个请求都成功以后才可以进行接下来的操作
all方法可以监控两个请求是否都完成,参数是数组,里面加入这些请求,当所有的子Promise都完成,该Promise完成,,.then返回值是一个Promise实例(对象)
Promise.all([
new Promise((resolve,reject)=>{
$.ajax({
url:'url1',
success:function(data1){
resolve(data1)
}
})
}),
new Promise((resolve,reject)=>{
$.ajax({
url:'url2',
success:function(data2){
resolve(data2)
}
})
})
]).then(results => {
results[0]
results[1]
}).catch(reason=>{
})
Promise.all2 = (arr) => {
return new Promise((resolve, reject) => {
if (!Array.isArray(arr)) {
return '';
}
if (arr.length === 0) {
resolve(arr);
}
let count = 0;
let arrRes = [];
for (let i = 0; i < arr.length; i++) {
// arr[i] 不一定是promise,所以需要包装一下
Promise.resolve(arr[i]).then(
res => {
// 通过索引来记录,保证结果数组按传入顺序返回,不能push
arrRes[i] = res;
count++;
if (count === arr.length) {
resolve(arrRes);
}
},
// 拒绝
err => {
reject(err);
})
}
})
}
promise.race
race方法则是它的子Promise中任意一个完成就算全部完成
Promise.race2 = (arr) => {
return new Promise((resolve, reject) => {
if ((!Array.isArray(arr))) {
reject('');
}
for (let i = 0; i < arr.length; i++) {
Promise.resolve(arr[i]).then((res) => {
resolve(res);
}, err => {
reject(err);
})
}
})
}
25.1. promise 打印题:
promise 打印顺序相关的题目
-
async
函数中await
的new Promise
要是没有返回值(resolve/reject)的话则不执行后面的内容 -
如果在async函数中抛出了错误,则终止错误结果,不会继续向下执行。
-
.then
或者.catch
的参数期望是函数,传入非函数则会发生值透传: 将resolve()
的值直接传到最后一个then
里。 -
.then
或者.catch
中return
一个error
对象并不会抛出错误,所以不会被后续的.catch
捕获。 -
.finally()
方法不管Promise
对象最后的状态如何都会执行 -
.finally()
方法的回调函数不接受任何的参数,也就是说你在.finally()
函数中是没法知道Promise
最终的状态是resolved
还是rejected
的 -
.finally() 它最终返回的默认会是一个上一次的Promise对象值,不过如果抛出的是一个异常则返回异常的
Promise
对象。
promise错误全局捕获
window.addEventListener('unhandlerejection',e => {
console.error(e.promise)
})
26. async和await
async和awiat实际上是Generator的语法糖,async关键字代表后面的函数中有异步操作,await表示等待一个异步方法执行完成。它等待的是一个Promise
async函数返回一个Promise对象,因此async函数通过return 返回的值,会成为then方法中回调函数的参数
async function funcA() {
return 'hello!';
}
funcA().then(value => {
console.log(value);
})
// hello!
单独的async和promise是一样的
async 函数被调用后就立即执行,但是一旦遇到
await `就会先返回,等到异步操作执行完成,再接着执行函数体内后面的语句。
可以说,async
函数完全可以看作多个异步操作,包装成的一个Promise 对象,而await
命令就是内部then
命令的语法糖。
async function func() {
console.log('async function is running!');
const num1 = await 200;
console.log(`num1 is ${num1}`);
const num2 = await num1+ 100;
console.log(`num2 is ${num2}`);
const num3 = await num2 + 100;
console.log(`num3 is ${num3}`);
}
func();
console.log('run me before await!');
// async function is running!
// run me before await!
// num1 is 200
// num2 is 300
// num3 is 400
一道打印题目
await的作用:等待后面函数的完成,完成后将他下一行及后面的内容推入微队列
async function asy1() {
console.log('1')
await asy2()
console.log(2)
}
const asy2 = async () => {
// 这里 await setTimeout(),setTimeout 函数同步调用完成后,将await下一行推入微任务队列,现在
// 下一行没有内容,那就是把 这个函数的完成 推入微队列,也就是 asy2会第一个进入微队列
await setTimeout(() => {
Promise.resolve().then(() => {
console.log(3)
})
console.log(4)
}, 0)
}
const asy3 = async () => {
Promise.resolve().then(() => {
console.log(6)
})
}
asy1()
console.log(7)
asy3()
// 1 7 6 2 4 3
27. fetch/ajax/axois
27.1. ajax:
Ajax
(Asynchronous JavaScript and XML)是一种使用多种技术(包括 XMLHttpRequest
(XHR)对象)在后台与服务器进行异步数据交换,而无需重新加载整个网页的 Web 开发技术。
XMLHttpRequest
是实现 Ajax
技术的关键对象之一。通过创建 XMLHttpRequest
对象,我们可以使用 JavaScript 向服务器发送请求,并处理服务器返回的响应。
简单来说,XMLHttpRequest
是实现 Ajax
的一种底层机制或工具。Ajax
是一个更广泛的概念,涵盖了使用包括 XMLHttpRequest
在内的技术来实现异步数据交互的方法和模式。
ajax的优点:可以实现局部刷新页面,即在页面不刷新的情况下获取数据。
ajax的缺点:如果网速慢,则会出现ajax请求缓慢,页面空白的情况,对客户的体验不好。ajax请求不利于搜索引擎优化,一般搜不到ajax添加到页面的信息!
解决的办法:可以先用服务器渲染。
//手动实现ajax
function ajax(method,url,data){
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if (xhr.readyState === 4) {
//304 表示请求内容未发生改变,可直接从缓存中读取
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
console.log("请求已经完成,响应已经就绪")
console.log(xhr.responseText);
}
}
}
xhr.open(method,url);
xhr.send(data);
xhr.abort() // 取消请求
}
ajax("post",'http://127.0.0.1:8080/user/show?name=hh');
// Promise封装Ajax请求
function ajax(method, url, data) {
var xhr = new XMLHttpRequest();
return new Promise(function (resolve, reject) {
xhr.onreadystatechange = function () {
if (xhr.readyState !== 4) return;
//304 表示请求内容未发生改变,可直接从缓存中读取
if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
resolve(xhr.responseText);
} else {
reject(xhr.statusText);
}
};
xhr.open(method, url);
xhr.send(data);
});
}
ajax('get',url,data).then((res) => {
const data = res;
}.catch(err => {
console.log(err)
})
27.2. fetch
fetchs是 JS
自带的发送请求的一个api
,类似于XMLHttpRequest
fetch是AJAX的替代品,是基于ES6中的promise实现。fetch不是ajax的进一步封装,未使用XMLHttpRequest,是原生js
// 通过fetch获取百度的错误提示页面
fetch('https://www.baidu.com/search/error.html', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
mode: 'no-cors', // 设置cors表示只能发送跨域的请求,no-cors表示跨不跨域都能发
body: JSON.stringify({
name: 'zhangsan',
age: 18
},
keepalive: true, // 使用 keepalive 选项
}).then((res)=>{
return res.json() // 返回的响应是JSON格式的,需要转换数据
}).then((res)=>{
console.log(res)
})
- 语法简单
- 可以被使用到更多地应用场景中
- 加强了代码的可维护性
- 避免回调地狱(Callback Hell)问题
keepalive
属性
keepalive
选项在 fetch
请求中的作用主要是允许在浏览器即将关闭或者用户即将离开当前页面时,仍然能够成功发送网络请求。这个选项的设计初衷是为了处理那些需要在页面生命周期结束时发送的统计或追踪数据的场景,比如用户的行为追踪数据、性能数据等
-
请求不会阻止页面关闭:使用了
keepalive
选项的请求不会阻止浏览器关闭页面,提升了用户体验。 -
数据量限制:为了保证功能的有效性和避免滥用,
keepalive
请求的数据大小有限制。最新的浏览器通常限制请求体的大小在 64KB 左右。
27.3. axios
Axios 的底层依赖会根据运行环境而有所不同:
在浏览器环境中,Axios 通常会优先使用 XMLHttpRequest
对象来发送请求。但它也可以使用 fetch
API (如果浏览器支持)。
在 Node.js 环境中,Axios 依赖于 Node.js 的 http
或 https
模块来发送请求。
axios是用于网络请求的第三方库,它是一个库。axios利用xhr进行了二次封装的请求库,xhr只是axios中的其中一个请求适配器,axios在nodejs端还有个http的请求适配器;axios = xhr + http;它返回一个Promise
。
- axios是一个基于Promise的HTTP库,可以用在浏览器和node.js中
- 拦截请求和响应
- 转换请求和响应数据
- 取消请求
安装:npm install axios 或者引用库
//引入axios
import axios from 'axios'
// 独立使用
//网络请求,然后得到请求结果在then中调用,默认内部调用promise,默认get请求
axios({
url:'url1',
method:'post',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
}).then(res => {
console.log(res)
})
//也可以这样
axios.get(url,{
params: {
ID: 12345
}
}).then(res => {
})
并发请求:多个请求都完成后才进行处理
axios.all([
axios({
url:'url1'
}),
axios({
url:'url2',
params:{
type:'pop'
}
})
]).then(results=>{
console.log(result)
})
创建axios实例:封装axios
axios拦截器:用于我们在发送每次请求或者得到响应后,进行对应的处理
//创建axios实例,根据指定配置创建一个新的axios,也就是每个axios都有自己的配置(如果没有自己的配置,你还创建他干啥)
var instance = axios.create({
baseURL: 'https://some-domain.com/api/',
timeout: 1000,
headers: {'X-product': 'h5'}
});
//设置request拦截器:成功或者失败
instance.interceptors.request.use(config=>{
//配置请求头,以下是一个示例,设置语言为简体中文
config.headers.lang = 'zh-CN'
//拦截下来的请求需要给人家返回回去,继续向服务器发送请求
//一般可用来对数据进行检查、转换
return config
},error=>{
})
//设置response拦截器:成功或者失败
instance.interceptors.response.use(res=>{
// 获取错误信息
const message = res.data.message
//通过响应码的不同进行不同的处理
if (code === 404) {
//响应码为404时的处理
}
。。。。
//使用完需要返回给需要这里响应数据的地方
return res
},error=>{
})
//options 接收 {method, url, params/data}
export default function({method,url,params) {
return instance({
method=,
url,
params: params,
data: data
})
}
// 编写接口请求函数:引入封装的axios请求,设置接口的地址、请求的方式、请求的数据等。
import axiosEp from '@/utils/request'
// 获取用户信息
export function getUserInfo(data) {
return axiosEp({
url: `/user/getUserInfo`, //请求的接口地址
method: 'post', //请求的方式
data //请求的数据
})
}
// 调用接口
const res = await getUserInfo({userId:1234567})
Vue中axios的使用
- 安装axios:
cnpm install axios
- 在main.js文件引入axios:
import Axios from 'axios'
- 将axios全局挂载到VUE原型上:
Vue.prototype.$http=Axios
axios是一个库,并不是vue中的第三方插件,使用时不能通过Vue.use()安装插件,需要在原型上进行绑定 - get方法发送请求
//方法一传递参数
this.$http.get('https://cnodejs.org/api/v1/topics',{
params: { //参数
page: this.page,
limit: this.limit,
},
}).then(res => { //请求成功后的处理函数
this.isLoading = false;
this.items = res.data.data;
console.log(this.items);
}).catch(err => { //请求失败后的处理函数
console.log(err)
})
//方法二传递参数
this.$http.get('https://cnodejs.org/api/v1/topics?page=1&limit=15')
- 注意请求成功后的处理使用了箭头函数,因此在该函数内的this指向的是vue实例,若使用普通函数则需要另外处理this。
请求取消
Axios 可以取消请求。官方文档指出有两种方法可以取消请求,分别是cancelToken
和AbortController
,cancelToken
已经被废弃 。示例代码如下:
- 如果在开始
axios request
之前执行了取消请求,则并不会发出真实的请求。
//
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// 取消请求
controller.abort()
使用canceltoken已经在新版本中呗废弃
/// 第一种
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.post("/user/12345", { name: "new name" }, { cancelToken: source.token });
source.cancel("Operation canceled by the user.");
// 第二种
const CancelToken = axios.cancelToken;
let cancel = null;
axios.get('ddd',{
params: {
id: 1
},
cancelToken: new CancelToken((c) => {
cancel = c;
}),
}
cancel()
28. 进制转换
十进制转为其他进制:toString()
number.toString(x)//数字转为x进制
var x = 110;
x.toString(2)//转为2进制
x.toString(8)//转为8进制
x.toString(16)//转为16进制
其他进制转为十进制:parseInt()
parseInt(number,x)//数字作为x进制转为十进制
var x = "110"//这是一个二进制的字符串表示
parseInt(x, 2)//把这个字符串当做二进制, 转为十进制
var x = "70"//这是一个八进制的字符串表示
parseInt(x, 8)//把这个字符串当做八进制, 转为十进制
var x = "ff"//这是一个十六进制的字符串表示
parseInt(x, 16)//把这个字符串当做十六进制, 转为十进制
29. set和get方法
对象中包含属性和方法
属性分为数据属性和访问器属性
数据属性:包含名字和value
访问器属性:包含get和set方法,可以定义读取和改变属性的时候调用的函数。通常的应用是:改变一个属性的值之后会导致其他属性发生变化,可以实现数据监听
情况一:对象已经创建,需要给上面添加set和get方法
/*
* 如果使用defineProperty定义set,get,默认configurable: false,enumerable: false
* Object.defineProperty(),第一个参数是对应的对象,第二个是访问器属性的名字,第三个是定义的方法
* _a下面的下划线表示只能通过对象方法访问的属性,而访问器属性"a"则定义了两个方法
* */
var obj={
_a:0;
b:1
};
Object.defineProperty(obj,"a",{
configurable:false,
enumerable:false,
set:function (newvalue) {
if(newvalue > 0){
this._a = newvalue;
this.b = 2;
}
},
get:function () {
return this._a;
}
});
obj.a = 1
alert(obj.b)//2
情况二:当创建对象时,使用这种写法
/*
* 当使用对象设置setget时,configurable: true,enumerable: true
* */
var obj1={
_a:0,
b:1,
set a(value){
this._a=value;
if(value > 0){
this.b = 2;
}
},
get a(){
return this._a;
}
};
obj1.a = 1
alert(obj1.b)//2
30. 怎样添加、移除、移动、复制、创建和查找节点?
1)创建新节点
createDocumentFragment() //创建一个DOM片段
createElement('div') //创建一个具体的元素
createTextNode() //创建一个文本节点
2)添加、移除、替换、插入
appendChild() //添加
removeChild() //移除
replaceChild() //替换
insertBefore() //插入
3)查找
通过ID获取(getElementById)
通过name属性(getElementsByName)注意:有s:elements
通过标签名(getElementsByTagName)注意:有s:elements
通过类名(getElementsByClassName):注意:有s:elements
获取html的方法(document.documentElement)
获取body的方法(document.body)
通过选择器获取一个元素(querySelector)
通过选择器获取一组元素(querySelectorAll)
31. document.write和innerHTML的区别:
document.write在页面加载完成后调用,会导致页面被重写。
innerHTML则是DOM页面元素的一个属性,代表该元素的html内容。你可以精确到某一个具体的元素来进行更改。
innerHTML很多情况下都优于document.write,其原因在于其允许更精确的控制要刷新页面的那一个部分。
32. new Function 和new Class的区别
主要区别
-
语法和语义:
class
提供了一种清晰、模块化的方式来定义构造函数和原型方法。通过class
关键字声明类使得代码更加直观易懂。 -
继承:使用
class
语法,可以通过extends
关键字更加简洁地实现继承。而在传统的函数式继承中,需要手动设置原型链。 -
严格模式:使用
class
语法定义的类的方法自动运行在严格模式下("use strict"
),而传统的构造函数则需要手动声明。
虽然new function()
和new class
都可以用来创建新的对象实例,但class
提供了更现代、更丰富的语法和特性,使得代码更加直观、易于管理和维护。然而,重要的是理解两者在 JavaScript 底层使用相同的原型继承机制。
33. 文档碎片(Document Fragment)
文档碎片(DocumentFragment)是一种在 DOM 操作中用于提高性能的技术。它是一个虚拟的 DOM 节点容器,可以在其中存储多个 DOM 元素,但不会直接在页面中渲染显示。文档碎片可以用来在内存中构建一组 DOM 元素,然后一次性将它们添加到文档中,从而减少 DOM 操作的重绘和重排,提高性能。
// 创建文档碎片
var fragment = document.createDocumentFragment();
// 创建一些 DOM 元素并添加到文档碎片中
var element1 = document.createElement('div');
element1.textContent = 'Element 1';
fragment.appendChild(element1);
var element2 = document.createElement('div');
element2.textContent = 'Element 2';
fragment.appendChild(element2);
// 将文档碎片一次性添加到文档中
document.body.appendChild(fragment);