一、 js基本数据类型和引用数据类型
js基本数据类型:Number、String、Boolean、Null、undefined、Symbol(es6)
引用数据类型:Object
注:数组、函数、正则表达式都是对象。
1、基本数据类型存放在栈中
基本数据类型存放在栈中,数据大小确定,内存空间大小可以分配,是直接按值存放的,所以可以直接按值访问。
这种基本数据类型赋值的过程如下图所示:
2、引用数据类型存放在堆中
引用数据类型存放在堆中,每个空间大小不一样,要根据情况 进行分配。
变量保存的是在栈内存中的一个指针,该指针指向堆内存,也就是说变量是存在栈中的一个地址,地址是该引用数据在堆中的地址。通过这个地址可以找到保存在堆内存中的对象。
复杂数据类型在声明之后, 会在堆内存中开辟出一块空间, 用来存放数据, 拿对象举例, 在我们新建一个对象之后, 会在堆内存中开辟一块空间, 用来存放对象里的数据, 而复杂数据类型跟简单数据类型的不同点就是在于,简单数据类型的变量指向的是内存中的数据, 而复杂数据类型指向的是其在内存中的地址,通过这个地址, 从而拿到地址中的数据, 因此, 如果将一个对象赋值给另一个对象的时候, 其实是把这个对象在内存空间中的地址传递给了另一个对象, 此时,他们共享内存中的同一块空间以及空间里的数据, 如果对其中一个对象的一个属性进行修改的话, 那么因为两个对象是共享一块地址一个数据的, 因此另一个对象中的属性也会被改变. 如果对其中一个对象重新赋值的话, 那么这个对象就会指向另一块内存空间, 就不在与另一个对象共享同一块内存了。
说明这两个引用数据类型指向的是同一个堆内存对象,obj1的值赋值给obj2得到值,给的是obj1在栈中的地址,实际上他们指向了堆中存储的同一个对象,所以修改obj2的name值,其实就是修改了堆内存中的那个对象,所以通过obj1也可以访问到。
var a = [1,2,3,4];
var b = a;
var c = a[0];// c是基本数据类型,存在栈中,修改不会对堆里的数据造成影响。
b[3] = 5;
c = 10;
console.log(c); // 10
console.log(a[0]); // 1
console.log(a); // [1,2,3,5]
console.log(b); // [1,2,3,5]
a是数组,是引用数据类型,var b = a是将a的地址赋值给了b,所以修改b,相当于修改了堆中的对象,所以a也会改变。c只是将a数组中的元素,存在栈中,是基本数据类型,修改栈中的数据不会影响堆。
symbol类型
https://juejin.cn/post/6925619440843227143
3、浅拷贝与深拷贝的区别和实现方式
简单点来说,就是假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝。如果B没变,那就是深拷贝。
深浅拷贝区别:
浅拷贝的时候如果数据是基本数据类型,那么就如同直接赋值那种,会拷贝其本身,如果除了基本数据类型之外还有一层对象,那么对于浅拷贝而言就只能拷贝其引用,对象的改变会反应到拷贝对象上;但是深拷贝就会拷贝多层,即使是嵌套了对象,也会都拷贝出来。
深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
浅拷贝常用的实现方法:
(1)for···in只循环第一层
// 只复制第一层的浅拷贝
function simpleCopy(obj1) {
var obj2 = Array.isArray(obj1) ? [] : {};
for (let i in obj1) {
obj2[i] = obj1[i];
}
return obj2;
}
var obj1 = {
a: 1,
b: 2,
c: {
d: 3
}
}
var obj2 = simpleCopy(obj1);
obj2.a = 3;
obj2.c.d = 4;
alert(obj1.a); // 1
alert(obj2.a); // 3
alert(obj1.c.d); // 4
alert(obj2.c.d); // 4
(2)Object.assign方法
let obj1 = {
a: 1,
b: {c: 2}
}
let obj2 = Object.assign({}, obj1);
obj2.a = 11;
obj2.b.c = 22;
console.log(obj1);// {a: 1, b: {c: 22}}
console.log(obj2);// {a: 11, b: {c: 22}}
注意点!严格来说,Object.assign() 既不是深拷贝,也不是浅拷贝——而是第一级属性深拷贝,第一级以下的级别属性浅拷贝。
(3)es6的扩展运算符...
https://www.cnblogs.com/leise/p/15005015.html
对象的一般格式为:
// key是键,value是值
let obj = { key :value }
var obj = {
a: 1,
b: {
a: 2
}
}
var obj1 = {...obj};
obj1.b.a = 3;
console.log(obj.b.a) // 3
1、当value是基本数据类型,比如String,Number,Boolean时,是可以使用拓展运算符进行深拷贝的。比如:
// value是基本数据类型
let oldObj = { id: 1 }
let newObj = { ...oldObj }
oldObj.id = 2
console.log(oldObj) // {id: 2}
console.log(newObj) // {id: 1} // 不随着oldObj的改变而改变
2、但是,当value是引用类型,比如Object,Array,这时使用拓展运算符进行深拷贝,得到的结果是和深拷贝的概念有矛盾的。主要是因为引用类型进行深拷贝也只是拷贝了引用地址。比如:
// value是引用类型
let oldObj = { type: { id: 1 } }
let newObj = { ...oldObj } // 此时拷贝了{id : 1}的引用地址
oldObj.type.id = 2 // 改变引用对象里面的值
console.log(oldObj) // {type: {id: 2}}
console.log(newObj) // {type: {id: 2}} 随着oldObj的改变而改变
oldObj.type = { id: 3 } // 改变引用的对象,实际改变了引用对象的地址
console.log(oldObj) // {type: {id: 3}}
console.log(newObj) // {type: {id: 2}} 不随着oldObj的改变而改变
总结:深拷贝最好使用lodash的cloneDeep方法或者JSON数据转换。
另外,关于Object.assign()语法进行对象的拷贝,里面的坑与拓展运算符的坑一样。
实现深拷贝的常用方法:
(1)采用递归去拷贝所有层级属性
function deepClone(obj){
let objClone = Array.isArray(obj)?[]:{};
if(obj && typeof obj==="object"){
for(key in obj){
if(obj.hasOwnProperty(key)){//这个方法可以用来检测一个对象是否含有特定的自身属性,忽略继承的属性。
//判断ojb子元素是否为对象,如果是,递归复制
if(obj[key]&&typeof obj[key] ==="object"){
objClone[key] = deepClone(obj[key]);
}else{
//如果不是,简单复制
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
let a=[1,2,3,4],
b=deepClone(a);
a[0]=2;
console.log(a,b);
(2) 通过JSON对象来实现深拷贝
function deepClone2(obj) {
var _obj = JSON.stringify(obj),
objClone = JSON.parse(_obj);
return objClone;
}
缺点:
1、如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式。而不是时间对象;
2、如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象;
3、如果obj里有function、undefined、symbol,则序列化的结果会把这些属性忽略;
4、如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null。
(3)通过jQuery的extend方法实现深拷贝
var array = [1,2,3,4];
var newArray = $.extend(true,[],array); // true为深拷贝,false为浅拷贝
(4)lodash函数库实现深拷贝
Lodash 是一个一致性、模块化、高性能的 JavaScript 实用工具库。
lodash是一个javascript库,也是Node JS的常用模块,它内部封装了诸多对字符串、数组、对象等常见数据类型的处理函数,其中部分是目前 ECMAScript 尚未制定的规范,但同时被业界所认可的辅助函数。
let result = _.cloneDeep(test)
4、判断基本数据类型和引用数据类型
Typeof
typeof操作符是检测基本类型的最佳工具。
Instanceof
instanceof用于检测引用类型,可以检测到它是什么类型的实例。
instanceof 检测一个对象A是不是另一个对象B的实例的原理是:查看对象B的prototype指向的对象是否在对象A的[[prototype]]链上。如果在,则返回true,如果不在则返回false。不过有一个特殊的情况,当对象B的prototype为null将会报错(类似于空指针异常)。
Constructor
constructor属性返回对创建此对象的数组函数的引用。可以用于检测自定义类型。
Object.prototype.toString.call(obj)
推荐使用:Object.prototype.toString.call(obj)
原理:调用从Object继承来的原始的toString()方法
4、数据类型转换
隐式转换
1.undefined与null相等,但不恒等(===)
2.一个是number一个是string时,会尝试将string转换为number
3.隐式转换将boolean转换为number,0或1
4.隐式转换将Object转换成number或string,取决于另外一个对比量的类型
5.对于0、空字符串的判断,建议使用 “===” 。
6.“==”会对不同类型值进行类型转换再判断,“===”则不会。它会先判断两边的值类型,类型不匹配时直接为false。
注意:
!{}=={} // false
因为obj.valueOf() //object
obj.toString() //"[object Object]",为长度15的字符串
所以!{}=={}结果为false;
而obj == '[object Object]'结果为true;
![] == [] // true
![]会转换为布尔类型false;
在javascript的判断规则中:如果一方为布尔类型会先转换为数字;
则左边转换为数字0;而右边为对象
1、所有对象先调用valueOf()方法,如果此方法返回的是原始值,则对象转为这个原始值。
2、如果valueOf方法返回的不是原始值,则调用toString方法,如果
toString方法返回的是原始值吗,则对象转换为这个原始值。
3、如果valueOf和toString方法均没有返回原始值,则抛出TypeError异
常。
显示转换
显示转换一般指使用Number、String和Boolean三个构造函数,手动将各种类型的值,转换成数字、字符串或者布尔值。
Number:
String:
Boolean:
使用总,!!相当于Boolean:
Number、String、Boolean转换对象时主要使用了对象内部的valueOf和toString方法进行转换。
Number转换对象:
1.先调用对象自身的valueOf方法。如果返回原始类型的值,则直接对该值使用Number函数,返回结果。
2.如果valueOf返回的还是对象,继续调用对象自身的toString方法。如果toString返回原始类型的值,则对该值使用Number函数,返回结果。
3.如果toString返回的还是对象,报错。
String转换对象
1.先调用对象自身的toString方法。如果返回原始类型的值,则对该值使用String函数,返回结果。
2.如果toString返回的是对象,继续调用valueOf方法。如果valueOf返回原始类型的值,则对该值使用String函数,返回结果。
3.如果valueOf返回的还是对象,报错。
Boolean转换对象
Boolean转换对象很特别,除了以下六个值转换为false,其他都为true
二、JS判断数组的六种方法详解
① instanceof 操作符判断
用法:arr instanceof Array
instanceof 主要是用来判断某个实例是否属于某个对象
用法:arr instanceof Array
instanceof 主要是用来判断某个实例是否属于某个对象
那么我们用instanceof 来判断数组的方法如下:
但是 instanceof 会有一个问题,它的问题在于假定只有一个全局执行的环境。如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的Array构造函数。如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有不同的构造函数。
②对象构造函数的 constructor判断
用法:arr.constructor === Array
Object的每个实例都有构造函数 constructor,用于保存着创建当前对象的函数
如上所示,obj 的实例 o1 的 constructor 跟 obj 对象是相等的
那么我们就可以用此来判断数组了
③Array 原型链上的 isPrototypeOf
用法:Array.prototype.isPrototypeOf(arr)
Array.prototype 属性表示 Array 构造函数的原型
其中有一个方法是 isPrototypeOf() 用于测试一个对象是否存在于另一个对象的原型链上。
④Object.getPrototypeOf
用法:Object.getPrototypeOf(arr) === Array.prototype
Object.getPrototypeOf() 方法返回指定对象的原型
所以只要跟Array的原型比较即可
⑤Object.prototype.toString
用法:Object.prototype.toString.call(arr) === '[object Array]'
虽然Array也继承自Object,但js在Array.prototype上重写了toString,而我们通过toString.call(arr)实际上是通过原型链调用了。
⑥Array.isArray
用法:Array.isArray(arr)
ES5中新增了Array.isArray方法,IE8及以下不支持
Array.isArray ( arg )
isArray 函数需要一个参数 arg,如果参数是个对象并且 class 内部属性是 "Array", 返回布尔值 true;否则它返回 false。采用如下步骤:
如果 Type(arg) 不是 Object, 返回 false。
如果 arg 的 [[Class]] 内部属性值是 "Array", 则返回 true。
返回 false.
instanceof的原理:
instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。
三、创建对象的几种方式
1、字面量创建 new Object()
2、构造函数创建
3、Object.create创建
new Object()与Object.create()的区别:
new Object() 是使用构造方法创造对象,新建一个对象实例,继承原对象的prototype属性。
Object.create(null) 创建的对象是一个空对象,在该对象上没有继承
Object.prototype 原型链上的属性或者方法。
Object.create({})继承Object.prototype上的方法。
Object.create()是将对象继承到proto属性上,原型链上没有任何属性,也就是没有继承Object的任何东西。
语法:
Object.create(proto, [propertiesObject])
proto:新创建对象的原型对象。
propertiesObject:可选。该参数对象是一组属性与值,该对象的属性名称将是新创建的对象的属性名。
四、new操作符的过程:
1、创建一个新的空的对象。
2、将构造函数的作用域赋给新对象(因此this就指向了这个新对象)。
3、执行构造函数中的代码(为这个新对象添加属性)。
4、如果这个函数有返回值,则返回;否则,就会默认返回新对象。
function myNew() {
// 由于arguments是类数组,我们不能直接使用shift方法,我们可以使用call来调用Array上的shift方法,获取constr
var constr = Array.prototype.shift.call(arguments);
var obj = Object.create(constr.prototype);// 新对象使用构造函数的原型
var result = constr.apply(obj, arguments);// 执行构造函数中的代码(为这个新对象添加属性)
// new这个关键字,并不是所有返回值都原封不动地返回的。如果返回的是undefined,null以及基本类型的时候,都会返回新的对象;
// 而只有返回对象的时候,才会返回构造函数的返回值。
return result instanceof Object ? result : obj;
}
五、谈一谈你对作用域、作用域链的理解。
作用域:作用域就是变量与函数的可访问范围。在JavaScript中,变量的作用域有全局作用域和局部作用域两种。
作用域就是定义变量的区域,它有一套访问变量的规则,根据这套规则来管理浏览器引擎如何在当前作用域和嵌套作用域中中根据变量(标识符)进行变量查找。作用域链:作用域链保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境中的变量和函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找。
六、谈一谈对闭包的理解,以及使用场景
闭包是指有权访问另一个函数作用域中变量或方法的函数,创建闭包的方式就是在一个函数内创建闭包函数,通过闭包函数访问这个函数的局部变量, 利用闭包可以突破作用链域的特性,将函数内部的变量和方法传递到外部。
通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
闭包的用处:
1.可以读取函数内部的变量;
2.可以让这些变量的值始终保持在内存中。但是不能滥用闭包,容易造成内存泄露。
闭包案例使用
function func(){
let n=0;
return function (){
n++;
console.log(n)
}
}
let a=func();
a() //1
a()//2
function outerFn(){
var i = 0;
function innerFn(){
i++;
console.log(i);
}
return innerFn;
}
var inner = outerFn(); //每次外部函数执行的时候,都会开辟一块内存空间,外部函数的地址不同,都会重新创建一个新的地址
inner();
inner();
inner();
var inner2 = outerFn();
inner2();
inner2();
inner2(); //1 2 3 1 2 3
var i = 0;
function outerFn(){
function innnerFn(){
i++;
console.log(i);
}
return innnerFn;
}
var inner1 = outerFn();
var inner2 = outerFn();
inner1();
inner2();
inner1();
inner2(); //1 2 3 4
function fun(n,o) {
console.log(o);
return {
fun:function(m) {
return fun(m,n);
}
};
}
var a = fun(0); //undefined
a.fun(1); //0
a.fun(2); //0
a.fun(3); //0
var b = fun(0).fun(1).fun(2).fun(3); //undefined 0 1 2
var c = fun(0).fun(1);
c.fun(2);
c.fun(3); //undefined 0 1 1
什么是内存泄漏
内存泄露是指当一块内存不再被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或者内存池的现象。内存泄漏可能会导致应用程序卡顿或者崩溃。
常见的内存泄露:
1、意外的全局变量
解决办法:在js文件开头添加 ‘use strict',开启严格模式。(或者一般将使用过后的全局变量设置为 null 或者将它重新赋值,这个会涉及的缓存的问题,需要注意)
计时器和回调函数timers
解决方式:当不需要interval或者timeout的时候,调用clearInterval或者clearTimeoutDOM泄漏
1)给DOM对象添加的属性是一个对象的引用 解决方法:在window.onload时间中加上 document.getElementById('id').diyProp = null;
2)元素引用没有清理 解决方法: a = null;
3)事件的绑定没有移除 解决方法: 移除时间的监听js闭包
七、js常用的垃圾收集方式:
js具有自动垃圾收集机制,也就是说执行环境会负责执行代码执行过程中使用的内存。垃圾回收机制是周期性运行的。
1、标记清除法
当变量进入环境时, 就将这个变量标记为“进入环境”,从逻辑上讲,永远不能释放进入环境的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开时就标记为“离开环境”
垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量将被视为准备删除的变量, 原因是环境中的变量已无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
2、引用计数法
跟踪记录每个值被引用的次数,当声明一个变量并将引用类型的值赋值给该变量的时候+1,相反当包含这个值引用的变量又取得了另外一个值,则这个值的引用-1。当这个值的引用次数为0时, 说明没有办法再访问这个值了。因而就可以将其占用的内存空间回收回来。
八、谈一谈你对this的理解,以及在各种环境下的this
在浏览器里,在全局范围内this指向window对象
在函数中,this永远指向最后调用他的那个对象(箭头函数除外)。
在构造函数中,this指向new出来的新对象。
call、apply、bind中的this被强绑定在指定的那个对象上。
箭头函数this为父作用域的this,不是调用时的this。
箭头函数this的注意事项
箭头函数的this,就是定义时所在的对象。内部的this就是定义时上层作用域中的this
一旦绑定了上下文,就不可改变(call、apply、bind 都不能改变箭头函数内部 this 的指向)。
由于this指向问题,所以:箭头函数不能当作构造函数,不能使用new命令。
箭头函数没有 arguments,需要手动使用 ...args 参数代替。
箭头函数不能用作 Generator 函数。
不管在什么情况下,箭头函数的this跟外层function的this一致,外层function的this指向谁,箭头函数的this就指向谁,如果外层不是function则指向window。
var obj={
id:123,
testFun:function(){
var a= ()=>console.log(this.id)
a();
}
}
//testFun的this指的是obj,则箭头函数的this指向obj。
obj.testFun() // 123
//testFun的this指向window,箭头函数的this指向window。
obj.testFun.apply(null) // undefined
call()、apply()、bind()的区别
都可以改变this的指向。
call(obj,params1,params2):第一个参数也是作为函数上下文的对象,但是后面传入的是一个参数列表,而不是单个数组。
apply(obj, [params1, params2]):一个是作为函数上下文的对象,另外一个是作为函数参数所组成的数组。
bind: 它和 call很相似,接受的参数有两部分,第一个参数是是作为函数上下文的对象,第二部分参数是个列表,可以接受多个参数。
bind返回的是函数,不会立即执行。call、apply会立即执行。
九、函数式编程
特点:
1、函数是"第一等公民"
是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
2、只用"表达式",不用"语句"
"表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。
3、没有"副作用"
指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。
函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。
4、不修改状态
函数式编程只是返回新的值,不修改系统变量。
5、引用透明
指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。
纯函数、高阶函数、闭包函数、函数柯里化
纯函数:对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。
高阶函数:高阶函数是对其他函数进行操作的函数,可以将它们作为参数或返回它们。简单来说,高阶函数是一个函数,它接受函数作为参数或函数作为输出返回。
map(),filter(),reduce()
函数柯里化:是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
实现函数柯里化
cosnt curry = (fn, arr = []) => {
return (...args) => {
return (a => {
a.length === fn.length ? fn(...a) : curry(fn, ...a)
})(...arr, ...args) //这里把arr和args摊开成一个数组赋值给a
}
}
实现一个add方法,使计算结果能够满足如下预期
add(1)(2)(3) = 6;
add(1, 2, 3)(4) = 10;
add(1)(2)(3)(4)(5) = 15;
function add() {
var _args = Array.prototype.slice.call(arguments);
var _adder = function () {
_args.push(...arguments);
return _adder;
}
_adder.toString = function() {
return _args.reduce(function(a, b){
return a + b;
});
}
return _adder;
}
十、js的EventLoop事件循环机制
js的执行机制:先执行主程序的任务,当主程序的所有任务都执行结束,再把任务队列中的任务放在主程序中执行。
步骤如下:
- 所有的同步任务都在一个执行线上执行,形成一个执行栈
- 主线程之外,还存在一个任务队列,只要异步任务有了执行结果,就在任务队列中放置一个事件
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件,那些对应的异步任务,就结束等待状态,进入执行栈,开始执行.
- 主线程不断的重复上面的三步.
事件循环
事件循环机制:
- 在此次 tick 中选择最先进入队列的任务( oldest task ),如果有则执行(一次)
- 检查是否存在 Microtasks ,如果存在则不停地执行,直至清空Microtask Queue
- 更新 render
- 主线程重复执行上述步骤。
宏任务:script(全局任务), setTimeout, setInterval, setImmediate, I/O, UI rendering.
微任务: Promise, process.nextTick, Object.observer, MutationObserver.
例子
十一、防抖和节流的区别及如何实现
鼠标移动事件onmousemove, 滚动滚动条事件onscroll,窗口大小改变事件onresize,瞬间的操作都会导致这些事件会被高频触发。 如果事件的回调函数较为复杂,就会导致响应跟不上触发,出现页面卡顿,假死现象。 在实时检查输入时,如果我们绑定onkeyup事件发请求去服务端检查,用户输入过程中,事件的触发频率也会很高,会导致大量的请求发出,响应速度会大大跟不上触发。
1、 debounce 防抖。
防抖策略是当事件被触发时,设定一个周期延迟动作,若期间又被触发,则重新设定周期,直到周期结束,执行动作。
js实现debounce
function debounce(fn) {
let timeout = null;
return function () {
clearTimeout(timeout);
timeout = setTimeout(() => {
fn.apply(this, arguments);
}, 500)
}
}
2、throttle 节流
节流的策略是,固定周期内,只执行一次动作,若有新事件触发,不执行。周期结束后,又有事件触发,开始新的周期。 节流策略也分前缘和延迟两种。与debounce类似,延迟是指 周期结束后执行动作,前缘是指执行动作后再开始周期
js实现throllte
function throttle(fn) {
let canRun = true;
if (!canRun) return;
return function () {
canRun = false;
setTimeout(() => {
fn.apply(this, arguments);
canRun = true;
}, 500)
}
}
防抖节流的使用场景
防抖:
1、登录、发短信等按钮避免用户点击太快,以致于发送了多次请求,需要防抖
2、调整浏览器窗口大小时,resize 次数过于频繁,造成计算过多,此时需要一次到位,就用到了防抖
3、文本编辑器实时保存,当无任何更改操作一秒后进行保存
4、一般可以使用在用户输入停止一段时间过后再去获取数据,而不是每次输入都去获取。
节流:
1、scroll 事件,每隔一秒计算一次位置信息等。
2、浏览器播放事件,每个一秒计算一次进度信息等。
十二、观察者模式和发布订阅模式区别和实现
观察者模式和发布订阅模式最大的区别就是发布订阅模式有个事件调度中心。
在观察者模式中,观察者需要直接订阅目标事件;在目标发出内容改变的事件后,直接接收事件并作出响应。
在发布订阅模式中,发布者和订阅者之间多了一个发布通道;一方面从发布者接收事件,另一方面向订阅者发布事件;订阅者需要从事件通道订阅事件
发布订阅模式的代码实现
//全局的发布--订阅对象
var Event = (function () {
var clientList = {},
listen,
trigger,
remove;
listen = function (key,fn) {
if(!clientList[key]){
clientList[key]=[];
}
clientList[key].push(fn);
};
trigger = function () {
var key = Array.prototype.shift.call(arguments),
fns = clientList[key];
if(!fns || fns.length===0){
return false;
}
for (var i =0,fn;fn=fns[i++];){
fn.apply(this,arguments);
}
};
remove = function (key,fn) {
var fns = clientList[key];
if(!fns){
return false
}
if(!fn){
fns && (fns.length=0);
}else{
for (var l=fns.length-1;l>=0;l--){
var _fn = fns[l];
if(_fn===fn){
fns.splice(l,1);
}
}
}
};
return {
listen:listen,
trigger:trigger,
remove:remove,
}
})();
Event.listen('squareMeter88',function (price) {
console.log("价额="+price);
});
Event.trigger('squareMeter88',2000000);
十三、原型、原型链、继承方式
原型及原型链
prototype:每个函数都会有prototype属性,而每一个javascript对象在创建的时候都会有一个与之关联的对象,就是我们所说的原型。每个实例对象都有proto,指向构造函数的prototype。
而每个原型上也有contructor指向构造函数。原型也有自己的proto指向Object.prototype->--proto__->null
原型链的访问规则:对象在访问属性或方法时,先检查自己的实例,如果存在就直接使用。如果不存在那么就去原型对象上去找,存在就直接使用,如果没有就顺着原型链一直往上查找,找到即使用,找不到就重复该过程直到原型链的顶端,如果还没有找到相应的属性或方法,就返回undefined,报错。
案例
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
语法:
object instanceof constructor
object:某个实例对象 constructor:某个构造函数
用来检测 constructor.prototype 是否存在于参数 object 的原型链上。
console.log(Object instanceof Function); // true
console.log(Object instanceof Object); // true
console.log(Function instanceof Function); // true
console.log(Function instanceof Object); // true
function Foo() {}
console.log(Object instanceof Foo); // false
1、原型链实现继承
实现方式:将子类的原型链指向父类的对象实例。
function Parent(){
this.name = "parent";
this.list = ['a'];
}
Parent.prototype.sayHi = function(){
console.log('hi');
}
function Child(){
}
Child.prototype = new Parent();
var child = new Child();
console.log(child.name);
child.sayHi();
var a = new Child();
var b = new Child();
a.list.push('b');
console.log(b.list); // ['a','b']
原理:子类实例child的proto指向Child的原型链prototype,而Child.prototype指向Parent类的对象实例,该父类对象实例的proto指向Parent.prototype,所以Child可继承Parent的构造函数属性、方法和原型链属性、方法
优点:可继承构造函数的属性,父类构造函数的属性,父类原型的属性
缺点:1、无法向父类构造函数传参;
2、且所有实例共享父类实例的属性,若父类共有属性为引用类型,一个子类
实例更改父类构造函数共有属性时会导致继承的共有属性发生变化;
2.构造函数继承
实现方式:在子类构造函数中使用call或者apply劫持父类构造函数方法,并传入参数
function Parent(name, id){
this.id = id;
this.name = name;
this.printName = function(){
console.log(this.name);
}
}
Parent.prototype.sayName = function(){
console.log(this.name);
};
function Child(name, id){
Parent.call(this, name, id);
// Parent.apply(this, arguments);
}
var child = new Child("jin", "1");
child.printName(); // jin
child.sayName() // Error
原理:使用call或者apply更改子类函数的作用域,使this执行父类构造函数,子类因此可以继承父类共有属性
优点:可解决原型链继承的缺点
缺点:不可继承父类的原型链方法,构造函数不可复用。
3、组合继承
实现方式:综合使用构造函数继承和原型链继承。
function Parent(name, id){
this.id = id;
this.name = name;
this.list = ['a'];
this.printName = function(){
console.log(this.name);
}
}
Parent.prototype.sayName = function(){
console.log(this.name);
};
function Child(name, id){
Parent.call(this, name, id);
// Parent.apply(this, arguments);
}
Child.prototype = new Parent();
var child = new Child("jin", "1");
child.printName(); // jin
child.sayName() // jin
var a = new Child();
var b = new Child();
a.list.push('b');
console.log(b.list); // ['a']
优点:可继承父类原型上的属性,且可传参;每个新实例引入的构造函数是私有的
缺点:会执行两次父类的构造函数,消耗较大内存,子类的构造函数会代替原型上的那个父类构造函数。
4、寄生组合式继承
原理:改进组合继承,利用寄生式继承的思想继承原型
function Parent(name){
this.name=name;
};
Parent.prototype.sayName=function(){
console.log(this.name)
};
function Child(name, age){
Parent.call(this, name);
this.age=age;
};
Child.prototype=Object.create(Parent.prototype);
Child.prototype.constructor=Child;
var instance = new Child('Jiang', 20);
instance.sayName();