一、js基本数据类型和引用数据类型
基本数据类型number string boolean undefined null symbol bigint 栈内存:值不变,值比较
引用数据类型Object对象(普通object function Array Date RegExp Math) 堆内存:引用地址的比较
es6新增的Set,Map的区别
- Set 和 Map 主要的应用场景在于 数据重组 和 数据储存。
- Set 是一种叫做集合的数据结构,Map 是一种叫做字典的数据结构。
集合 与 字典 的区别:
- 共同点:集合、字典 可以储存不重复的值
- 不同点:集合 是以 [value, value]的形式储存元素,字典 是以 [key, value] 的形式储存
注意: 数据类型和数据结构是不一样的
前端常见的数据结构
一.队列 二.栈 三.链表
四.集合
ES6已经为我们提供了Set的api,但是在某些时候(浏览器不支持的情况下),我们还是需要自己来实现Set的
class Set{ constructor(){ this.items = {}; } has(value){ // 判断 return this.items.hasOwnProperty(value); } add(value){ // 像集合中添加 if(!this.has(value)){ this.items[value] = value; } } remove(value){ // 删除集合中的某一项 if(this.has(value)){ delete this.items[value]; } } }
五.hash表 六.树 七.图
集合(Set):
ES6 新增的一种新的数据结构,类似于数组,成员唯一(内部元素没有重复的值)。且使用键对数据排序即顺序存储。
Set 本身是一种构造函数,用来生成 Set 数据结构。
Set 对象允许你储存任何类型的唯一值,无论是原始值或者是对象引用。
// 去重数组的重复对象
let arr = [1, 2, 3, 2, 1, 1]
[... new Set(arr)] // [1, 2, 3]
注意:向 Set 加入值的时候,不会发生类型转换,所以5和"5"是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是NaN等于自身,而精确相等运算符认为NaN不等于自身。
操作方法:
add(value):新增,相当于 array里的push。
delete(value):存在即删除集合中value。
has(value):判断集合中是否存在 value。
clear():清空集合。
便利方法:遍历方法(遍历顺序为插入顺序)
keys():返回一个包含集合中所有键的迭代器。
values():返回一个包含集合中所有值得迭代器。
entries():返回一个包含Set对象中所有元素得键值对迭代器。
forEach(callbackFn, thisArg):用于对集合成员执行callbackFn操作,如果提供了 thisArg 参数,回调中的this会是这个参数,没有返回值。
set放NaN会有几个?NaN全等于NaN吗?
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}
let set1 = new Set()
set1.add(5)
set1.add('5')
console.log([...set1]) // [5, "5"]
console.log(NaN == NaN)//false
字典(Map):
是一组键值对的结构,具有极快的查找速度。
操作方法:
set(key, value):向字典中添加新元素。
get(key):通过键查找特定的数值并返回。
has(key):判断字典中是否存在键key。
delete(key):通过键 key 从字典中移除对应的数据。
clear():将这个字典中的所有元素删除。
遍历方法:
Keys():将字典中包含的所有键名以迭代器形式返回。
values():将字典中包含的所有数值以迭代器形式返回。
entries():返回所有成员的迭代器。
forEach():遍历字典的所有成员。
二、typeof
返回值number string boolean undefined Object function symbol
function a(){}
console.log(typeof a )
console.log(typeof b )
console.log(typeof undefined )//undefined
console.log(typeof typeof xxx )//string
console.log(typeof NaN )
缺点?typeof无法区分数组和普通对象
三、typeof如何区分数组和普通对象?
1.Array.isArray()
2.a instanceof b 判断a的原型链上是否有b的原型
实现{} instanceof Object
function instance_of(a,b) {
let atype = Object.getPrototypeOf(a);
while (true) {
if(atype == null) return false
if(atype == b.prototype){
return true
}
atype = Object.getPrototypeOf(atype);
}
}
console.log(instance_of({},Object))
console.log(instance_of({},Array))
3.Object.prototype.toString
console.log(Object.prototype.toString.call({}) == '[object Object]')
console.log(Object.prototype.toString.call([]) == '[object Array]')
console.log(Object.prototype.toString.call(new Date()) == '[object Date]')
为什么这样就能区分呢?于是我去看了一下toString方法的用法:toString方法返回反映这个对象的字符串。
那为什么不直接用obj.toString()呢?
console.log("jerry".toString());//jerry console.log((1).toString());//1 console.log([1,2].toString());//1,2 console.log(new Date().toString());//Wed Dec 21 2016 20:35:48 GMT+0800 (中国标准时间) console.log(function(){}.toString());//function (){} console.log(null.toString());//error console.log(undefined.toString());//error这是因为toString为Object的原型方法,而Array 、Function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(Function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串.....),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object上原型toString方法。
四、原型链
原型链:实例对象和原型对象之间的链接关系
为啥要用原型链?为了实现继承,简化代码,只要是链条上的内容,都可以访问到
原型链特点?就近原则;每个函数都存在prototype,并且默认原型都是Object的实例;每个实例对象都有_proto_,指向父函数的prototype
所以JavaScript 有隐式原型(__proto__)与显式原型(prototype)
JavaScript中任意对象都有一个内置属性[[prototype]],在ES5以前没有标准的方法访问这个内置属性,可是大多数浏览器都支持经过__proto__来访问。ES5中有了对于这个内置属性标准的Get方法Object.getPrototypeOf()
Object.prototype 这个对象是个例外,它的__proto__值为null
隐式原型与显式原型的关系
隐式原型指向建立这个对象的函数(constructor)的prototype浏览器
1.
__proto__
和constructor
每一个对象数据类型(普通的对象、实例、
prototype
......)也天生自带一个属性__proto__
,属性值是当前实例所属类的原型(prototype
)。原型对象中有一个属性constructor
, 它指向函数对象。
function Person() {}
var person = new Person()
console.log(person.__proto__ === Person.prototype)//true
console.log(Person.prototype.constructor===Person)//true
//顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
2.何为原型链
在JavaScript中万物都是对象,对象和对象之间也有关系,并不是孤立存在的。对象之间的继承关系,在JavaScript中是通过prototype对象指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条,专业术语称之为原型链。
当我们访问对象的一个属性或方法时,它会先在对象自身中寻找,如果有则直接使用,如果没有则会去原型对象中寻找,如果找到则直接使用。如果没有则去原型的原型中寻找,直到找到Object对象的原型,Object对象的原型没有原型,如果在Object原型中依然没有找到,则返回undefined。
Object是JS中所有对象数据类型的基类(最顶层的类)在Object.prototype上没有
__proto__
这个属性。
console.log(Object.prototype.__proto__ === null) // true
3.js的new操作符到底做了什么?
1、创建了一个空的js对象(即{})
2、将空对象的原型prototype指向构造函数的原型
3、将空对象作为构造函数的上下文(改变this指向)
4、对构造函数有返回值的判断
怎么实现?
/* create函数要接受不定量的参数,第一个参数是构造函数(也就是new操作符的目标函数),其余参数被构造函数使用。 new Create() 是一种js语法糖。我们可以用函数调用的方式模拟实现 */ function create(Con,...args){ //1、创建一个空的对象 let obj = {}; // let obj = Object.create({}); //2、将空对象的原型prototype指向构造函数的原型 Object.setPrototypeOf(obj,Con.prototype); // obj.__proto__ = Con.prototype //3、改变构造函数的上下文(this),并将剩余的参数传入 let result = Con.apply(obj,args); //4、在构造函数有返回值的情况进行判断 return result instanceof Object?result:obj; }
4.call() apply() bind()?
作用都是用来改变fn内部的this指向
fn.call(obj,1,2)立即执行
fn.bind(obj,1,2)等待执行
fn.apply(obj,[1,2])立即执行
五、i++与++i
i++:先输出后计算 ++i:先计算后输出(推荐)
let a = 1;
alert(a++);
let b = 1;
alert(++b);
六、闭包是什么?
闭包实际上是一种函数,就是有权访问某个函数里私有变量的函数。
闭包产生的时机:内层的作用域访问它外层函数作用域里的参数/变量/函数时,闭包就产生了。
闭包的不足:由于闭包会引用它外层函数作用域里的变量函数,因此,会比其他非闭包形式的函数占用更多内存,当外层函数执行完毕退回函数调用栈(call stack)的时候,外层函数作用域里变量因为被引用着,可能并不会被引擎的垃圾回收器回收,因而会引起内存泄漏。过度使用闭包会导致内存占用过多,甚至内存泄漏。
为什么要用到闭包?
外部函数调用之后,其变量对象应该被销毁,但是闭包的存在使我们任然可以访问外部函数的变量对象,让这些变量始终保持在内存之中。
function our(){
let a = '1'
return function(){
return a
}
}
var b = our()
console.log(b())
造成啥后果?内存泄漏
怎么解决?b = null,释放对闭包的引用
闭包应用场景?
闭包应用场景1–封闭作用域
JavaScript的GC机制
在JavaScript中,如果一个对象不再被引用,那么这个对象就会被GC回收,否则这个对象一直会保存在内存中。
封闭作用域又称为封闭空间,小闭包,匿名函数自调
技术最大的目的是:全局变量私有化优点:1、不污染全局空间;2、内部所有的临时变量执行完毕都会释放不占内存;3、可以保存全局数据;4、更新复杂变量
闭包应用场景2–作用域链
嵌套之间的函数会形成作用域链,每次对变量的访问实际上都是对整条作用域链的遍历查找,先查找最近的作用域,最后再查找全局作用域,如果在某个作用域找到了对量就会结束本次查找过程。
思考?对于作用域全局查找快-?还是局部作用域查找快?
局部作用域查找远远大于全局作用域查找的速度,所以高级程序设计一般尽量避免全局查找。
每次访问都是对作用域链的一次遍历查找,其中全局作用域是最耗时的。
解决方案:当前目标使用完以后,在退出作用域之前储存这个目标,就可以在下次取到上一次的目标。闭包应用场景3–保存作用域
保存作用域是一种更高级的闭包技术,如果函数嵌套函数,那么内部的那个函数将形成作用域闭包。好处就是让指令能够绑定一些全局数据去运行。
<button id="btn">点我</button>
(function(document){
var btn=document.getElementById("btn");
})(document)//传入了参数,第一次查找时是在全局查找,后面的直接使用,也就是保存了全局的数据,局部直接使用。优点:1、全局数据隐藏化;2、可以让某个指令运行时绑定一些隐藏的全局数据在身上。
一句话:将数据绑定在指令上运行,让指令不再依赖全局数据。闭包应用场景4–高级排他
闭包应用场景5–参数传递
闭包应用场景6–函数节流
闭包应用场景7–延长局部变量的生命
所有如以下写法的都是闭包
function a(){
let b
return function(){
bxxx
}
}
七、toString和valueOf
共同点:为了解决js值运算和显示问题
区别
toString:返回当前对象的字符串形式
valueOf:返回该对象的原始值
console.log(Object.prototype.toString({a:'1'}))
console.log(Object.prototype.valueOf({a:'1'}))
调用优先级:隐式转换时会自动调用,强制转换为字符串时用toString,数值时用valueOf,当使用运算符时,valueOf高于toString
valueOf的应用?
let a = {
value: 0,
valueOf: function(){
this.value ++
return this.value
}
}
console.log(a==1 && a==2)//true
八、hasOwnProperty和isPrototypeOf
obj.hasOwnProperty():判断某个对象是否含有指定属性,无法检测原型链
Object.prototype.isPrototypeOf({}):判断Object的原型是否在{}的原型链上
九、setTiemeout与事件队列
for (var i = 0; i <= 100; i++) {
if (i === 1) {
setTimeout(function () {
console.log("timeout");
}, 0);
}
console.log(i);
}
当真正运行的时候你会发现,这个"timeout"文字会在整个for执行完之后再执行,而不是想当然的在某一步运行完之后执行。
JS的异步机制由事件循环和任务队列构成.JS本身是单线程语言,所谓异步依赖于浏览器或者操作系统等完成. JavaScript 主线程拥有一个执行栈以及一个任务队列,主线程会依次执行代码,当遇到函数时,会先将函数入栈,函数运行完毕后再将该函数出栈,直到所有代码执行完毕。
遇到异步操作(例如:setTimeout, AJAX)时,异步操作会由浏览器(OS)执行,浏览器会在这些任务完成后,将事先定义的回调函数推入主线程的任务队列(task queue)中,当主线程的执行栈清空之后会读取task queue中的回调函数,当task queue被读取完毕之后,主线程接着执行,从而进入一个无限的循环,这就是事件循环.
JavaScript是单线程执行的,无法同时执行多段代码。当某一段代码正在执行的时候,所有后续的任务都必须等待,形成一个队列。一旦当前任务执行完毕,再从队列中取出下一个任务,这也常被称为 “阻塞式执行”。所以一次鼠标点击,或是计时器到达时间点,或是Ajax请求完成触发了回调函数,这些事件处理程序或回调函数都不会立即运行,而是立即排队,一旦线程有空闲就执行。假如当前 JavaScript线程正在执行一段很耗时的代码,此时发生了一次鼠标点击,那么事件处理程序就被阻塞,用户也无法立即看到反馈,事件处理程序会被放入任务队列,直到前面的代码结束以后才会开始执行。如果代码中设定了一个 setTimeout,那么浏览器便会在合适的时间,将代码插入任务队列,如果这个时间设为 0,就代表立即插入队列,但不是立即执行,仍然要等待前面代码执行完毕。所以 setTimeout 并不能保证执行的时间,是否及时执行取决于 JavaScript 线程是拥挤还是空闲。
事件循环
例子如下:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
//执行结果:script start=>script end=>promise1 =>promise2=>setTimeout在JavaScript中,我们把任务分为同步任务和异步任务
同步和异步任务分别进入不同的执行”场所”,同步的进入主线程,异步的进入Event Table并注册函数
当指定的事情完成时,Event Table会将这个函数移入Event Queue
主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
上述过程会不断重复,也就是常说的Event Loop(事件循环)
我们发现,同样是异步任务,也分先后,promise要优于setTimeout,这就要引出下个知识点
宏任务和微任务
其实异步任务是包含宏任务和微任务,他们会加在同一个队列里面,只是执行顺序不一样而已。
宏任务:整体代码script、setTimeout、setInterval、setImmediate,postMessage、
微任务:原生Promise中then方法、process.nextTick、MutationObserver
对于宏任务和微任务我们只要记住以上几种就好。
执行顺序:同步任务>微观任务>宏观任务
关于setTimeout要补充的是,即便主线程为空,setTimeout(fn,0),0毫秒实际上也是达不到的。根据HTML的标准,最低是4毫秒。关于setInterval( fn , ms )来说并不是每隔ms就会执行一遍,而是经过ms后回调函数加入事件队列,所以setInterval未必准确。
十,页面回流与重绘
1.什么是回流?
当render tree中的一部分(或全部),因为元素的规模尺寸、布局、隐藏等改变 而需要重新构建,这就是回流(reflow)
每个页面至少回流一次,即页面首次加载
2.什么是重绘?
当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外 观、风格,而不影响布局(例如:background-color),则称为重绘(repaints)
特点:回流必将引起重绘,重绘不一定引起回流 回流比重绘的代价更高
当页面布局和几何属性改变时就需要回流
(1)添加或者删除可见的DOM元素; (2)元素位置改变; (3)元素尺寸改变——边距、填充、边框、宽度和高度 (4)内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变; (5)页面渲染初始化; (6)浏览器窗口尺寸改变——resize事件发生时;
重绘何时发生?
元素的属性或者样式发生变化。
到此,我们就知道如何减少回流和重绘了。
十一,ES6 ,ES7新特性?
ES6(2015)
1、Let和Const和var
let 声明的变量只在所在块级作用域中可用
const 如果声明的常量是基本类型的数据,不可重新赋值;如果声明的是引用数据类型,改其中的某个属性确实可以被修改掉,如何理解?
因为对象是引用类型的,P中保存的仅是对象的指针,这就意味着,const仅保证指针不发生改变,修改对象的属性不会改变对象的指针,所以是被允许的。也就是说const定义的引用类型只要指针不发生改变,其他的不论如何改变都是允许的。
我们试着修改一下指针,让P指向一个新对象,即使对象的内容没发生改变,指针改变也是不允许的。
2、Class(类)
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
//一个类必须有constructor方法
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
ES6的类,完全可以看作构造函数的另一种写法。
Class之间可以通过extends
关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。
class ColorPoint extends Point {}//相当于复制
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
3、箭头函数
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或 new.target。这些函数表达式更适用于那些本来需要匿名函数的地方,并且它们不能用作构造函数。
4、函数参数默认值
5、模板字符串const info = `My name is ${name}, I am ${age}`
6、解构赋值
7、模块化(Module)
在ES6之前,JS并没有模块化的概念,有的也只是社区定制的类似CommonJS和AMD之类的规则。例如基于CommonJS的NodeJS:
/ circle.js
// 输出
const { PI } = Math
export const area = (r) => PI * r ** 2
export const circumference = (r) => 2 * PI * r
// index.js
// 输入
import {
area
} = './circle.js'
8.Promise
romise 是ES6提供的一种异步解决方案,比回调函数更加清晰明了。
Promise 有三种状态,分别是:1.等待中(pending)2.完成了 (resolved)3.拒绝了(rejected)
这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 resolved 后,就不能再次改变
9、for …of
for…of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。
10、Symbol
symbol 是一种基本数据类型,Symbol()函数会返回symbol类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的symbol注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:“new Symbol()”。
11、Proxy/Reflect
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 Proxy 的方法相同。Reflect不是一个函数对象,因此它是不可构造的。
ES7(ES2016)
1、Array.prototype.includes()includes()
方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。
2、幂运算符**
3、模板字符串(Template string)
ES8(ES2017)
1、async/await
虽然Promise
可以解决回调地狱的问题,但是链式调用太多,则会变成另一种形式的回调地狱 —— 面条地狱,所以在ES8里则出现了Promise
的语法糖async/await
,专门解决这个问题。
2、Object.values() Object.entries()
3、padStart()
padStart()
方法用另一个字符串填充当前字符串(重复,如果需要的话),以便产生的字符串达到给定的长度。填充从当前字符串的开始(左侧)应用的。
const str1 = '5'
console.log(str1.padStart(2, '0')) // "05"
const fullNumber = '2034399002125581'
const last4Digits = fullNumber.slice(-4)
const maskedNumber = last4Digits.padStart(fullNumber.length, '*')
console.log(maskedNumber) // "************5581"
4、padEnd()
padEnd()
方法会用一个字符串填充当前字符串(如果需要的话则重复填充),返回填充后达到指定长度的字符串。从当前字符串的末尾(右侧)开始填充。
ES9(ES2018)
1、for await…of
for await...of 语句会在异步或者同步可迭代对象上创建一个迭代循环,包括 String,Array,Array-like 对象(比如arguments 或者NodeList),TypedArray,Map, Set和自定义的异步或者同步可迭代对象。其会调用自定义迭代钩子,并为每个不同属性的值执行语句。
配合迭代异步生成器,例子如下:
async function* asyncGenerator() {
var i = 0
while (i < 3) {
yield i++
}
}
(async function() {
for await (num of asyncGenerator()) {
console.log(num)
}
})()
// 0
// 1
// 2
2、对象扩展操作符
ES6中添加了数组的扩展操作符,让我们在操作数组时更加简便,美中不足的是并不支持对象扩展操作符,但是在ES9开始,这一功能也得到了支持,例如:
var obj1 = { foo: 'bar', x: 42 };
var obj2 = { foo: 'baz', y: 13 };
var clonedObj = { ...obj1 };
// 克隆后的对象: { foo: "bar", x: 42 }
var mergedObj = { ...obj1, ...obj2 };
// 合并后的对象: { foo: "baz", x: 42, y: 13 }
上面便是一个简便的浅拷贝。这里有一点小提示,就是Object.assign() 函数会触发 setters,而展开语法则不会。所以不能替换也不能模拟Object.assign() 。
如果存在相同的属性名,只有最后一个会生效。