一、ES6新特性
for of
箭头函数
javascript标签控制外层循环:break可用于循环或者switch,continue只能用域循环。
浮点数及大数精度问题:
https://www.runoob.com/w3cnote/js-precision-problem-and-solution.html
js解析机制
===、typeof、+、布尔值
1、const、let、var
- ES2015(ES6) 新增加了两个重要的 JavaScript 关键字:
let
和const
。let
声明的变量只在let
命令所在的代码块内有效。const
声明一个只读的常量,一旦声明,常量的值就不能改变。
ES6之前作用域
- 在 ES6 之前,JavaScript只有两种作用域: 在函数外声明的变量为全局变量,作用域是全局的,在函数内声明的变量为局部变量,作用域是局部的(函数内)。在 ES6之前,是没有块级作用域的概念的。使用
var
关键字声明的变量不具备块级作用域的特性,它在{}
外依然能被访问到。 - ES6 可以使用
let
关键字来实现块级作用域。let
声明的变量只在let
命令所在的代码块{}
内有效,在{}
之外不能访问。在函数体内使用var
和let
关键字声明的变量有点类似。它们的作用域都是局部的;在函数体外或代码块外使用var
和let
关键字声明的变量也有点类似。它们的作用域都是全局的。 - const定义常量与使用let 定义的变量相似:二者都是块级作用域;都不能和它所在作用域内的其他变量或函数拥有相同的名称。
注意:
- 使用var关键字声明的全局作用域变量属于window对象。
使用let关键字声明的全局作用域变量不属于window对象。
使用var关键字声明的变量在任何地方都可以修改。
在相同的作用域或块级作用域中,不能使用let关键字来重置var或let关键字声明的变量。
在相同的作用域或块级作用域中,不能使用const关键字来重置var和let或const关键字声明的变量。
let关键字在不同作用域,或不同块级作用域中是可以重新声明赋值的。
const关键字在不同作用域,或不同块级作用域中是可以重新声明赋值的。
var关键字定义的变量可以先使用后声明。
let关键字定义的变量需要先声明再使用。
const关键字定义的常量,声明时必须进行初始化,且初始化后不可再修改。
总结:
1、块级作用域
使用 var
关键字声明的变量不具备块级作用域的特性,它在 {}
外依然能被访问到。
let
声明的变量只在let
命令所在的代码块 {}
内有效,在{}
之外不能访问。
没有块级作用域时:
缺点1:内部变量可能会覆盖外层的变量
缺点2:for循环中的计数变量泄露为全局变量。
2、变量提升
var关键字定义的变量可以先使用后声明。
let关键字定义的变量需要先声明再使用。
const关键字定义的常量,声明时必须进行初始化,且初始化后不可再修改。
3、重复声明
使用var关键字声明的变量在任何地方都可以修改。
在相同的作用域或块级作用域中,不能使用let关键字来重置var或let关键字声明的变量。
在相同的作用域或块级作用域中,不能使用const关键字来重置var和let或const关键字声明的变量。
let关键字在不同作用域,或不同块级作用域中是可以重新声明赋值的。
const关键字在不同作用域,或不同块级作用域中是可以重新声明赋值的。
4、window对象的关系
使用var关键字声明的全局作用域变量属于window对象。
使用let关键字声明的全局作用域变量不属于window对象。
const:
对于数组和对象中的元素进行修改,不算对常量的改变
const变量保存的变量指向的值的内存地址。对于简单的数据(数值,字符串,布尔值)值保存在变量指向的内存地址。
对于复合数据类型,变量指向的内存地址,只是该对象的地址,其中的内容发生变化不会使该对象的内存地址发生变化。
https://blog.csdn.net/z18237613052/article/details/114029023
展开运算符:
- 数组传入函数参数/把参数转换为数组:
function f(a, b, c) {
console.log(a, b, c);
}
let arr = [1, 2, 3];
f(...arr);//1 2 3
f.apply(null, arr);//1 2 3
f(1, 2, 3);//1 2 3
function f2(...args){
console.log(args)
}
f2(1,2,3) // [1,2,3]
function f3(){
console.log(Array.from(arguments))
}
f3(1,2,3) // [1,2,3]
- 数组、对象解构赋值
数组解构赋值:数组中按顺序匹配,将1赋值给c,剩下的元素以数组形式赋值给展开运算符作用的变量。
let a = [1,2,3,4,5,6]
let [c,...d] = a
console.log(c); // 1
console.log(d); // [2,3,4,5,6]
//展开运算符必须放在最后一位
对象解构赋值:对象中定义一个变量a,一个变量b,在被解构的对象中进行匹配,匹配到与变量名相同的key值,就把value赋值给该变量,匹配与顺序无关,剩下的键值对以对象形式赋值给展开运算符作用的变量。
let { aa, bb, ...rest } = {
aa: "11",
bb: "22",
cc: "33",
dd: "44",
ee: "55",
};
console.log(aa);
console.log(bb);
console.log(rest);
冒号前是用于匹配的变量名,冒号后是变量名的别名,匹配到的值最后会赋给别名,此时的a是没有值的,输出会报错。
let { a: c, b, ...rest } = {
a: 1111,
b: 2222,
c: 3333,
d: 4444,
e: 5555,
f: 6666,
};
// console.log(a);//Uncaught ReferenceError: a is not defined
console.log(b);//2222
console.log(c);//1111
// console.log(d);//Uncaught ReferenceError: d is not defined
console.log(rest);//{c: 3333, d: 4444, e: 5555, f: 6666}
- 数组、对象构造合并
数组和数组:
let a = [1, 2, 3];
let b = [4, 5, 6];
let c = [...a, ...b];
console.log(c);//[1, 2, 3, 4, 5, 6]
let d = Array.prototype.push.apply(a, b);
console.log(a);//[1, 2, 3, 4, 5, 6]
a.push(...b);
console.log(a); //[1, 2, 3, 4, 5, 6, 4, 5, 6]
对象和对象:
let x = {
name: 'autumn'
}
let y = {
age: 18
}
let z = {...x,...y}
console.log(z)//{name: "autumn", age: 18}
数组和对象:
let x = [{
name: 'autumn'
}]
let y = {
name: 'wscats'
}
let z = [...x, y];
console.log(z);//[{name: "autumn"}, {name: "wscats"}]
对象和对象:
let x = {
name: ['autumn','wscats'],
age:18
}
let y = {
...x,//name: ['autumn','wscats'],age:18
arr: [...x.name]//['autumn','wscats']
}
console.log(y)//{name: Array(2), age: 18, arr: Array(2)}
数组和字符串:
let x = ['autumn'];
let y = 'wscats';
let z = [...x, y];
console.log(z);//["autumn", "wscats"]
- 浅拷贝
//数组
var a = [1,2,4]
var b = [...a]
a.push(6)
console.log(b) // [1,2,4]
//对象
var a = {a:1}
var b = {...a}
a.a = 5
console.log(b.a) // 1
- 具有Iterator接口的对象转换为数组
var nodelist = document.querySelectorAll('div');
console.log([...nodelist]) // 转化成数组
var map = new Map([[1,11],[2,22],[3,33]]);
console.log([...map.keys()]); // [1,2,3]
- 帮助完成vuex中函数的映射
来源:https://www.jianshu.com/p/a432a9e78949
来源:https://www.jianshu.com/p/3935a80342a0
来源:https://blog.csdn.net/weixin_41615439/article/details/85319618
二、JavaScript执行机制
- JavaScript是单线程的语言,无论以什么方式实现异步,都只是用同步的方法去模拟,并不是真的异步。
- JavaScript的事件循坏是JS实现异步的方式,也是JS的执行机制。
1、同步任务与异步任务
JavaScript是按照语句出现的顺序执行的,一行执行完才会执行下一行,比如在alert后写log,加入不关闭弹窗,是不会输出log的。但有时一些任务耗时过长,如果按照顺序,可能会长时间等待,由此将任务分为两类:同步任务和异步任务。
诸如页面骨架、页面元素的加载为同步任务,图片、音乐的加载就是异步任务。回调代码才是异步执行的。
基本的执行机制为:
- 同步任务和异步任务进入不同的线程,同步任务进入主线程,开始执行,异步任务进入Event Table注册回调函数。
- 当指定的事情完成是,Event Table将该函数放入到任务队列Event Queue。
- 当主线程任务执行完毕,执行栈为空时,会去Event Queue读取对应的函数进入主函数执行。
以上过程重复,称为事件循环。
对于主线程执行栈何时为空,有专门的线程(monitoring process)会进行检查。
let data = [];
$.ajax({
url:www.javascript.com,
data:data,
success:() => {
console.log('发送成功!');
}
})
console.log('代码执行结束');
- ajax进入Event Table,注册
success
函数。 - 执行
console.log('代码执行结束');
- ajax事件完成后,将
success
置入Event Queue。 - 主线程从Event Queue读取回调函数并执行。
2、setTimeout、setInterval、Promise、process.nextTick(callback)
setTimeout(() => {
task()
},3000)
sleep(10000000)
setTimeout
进入Event Table注册task()
回调函数- 执行
sleep(10000000)
- 3秒计时完毕,Event Table将
task()
移入Event Queue,但由于sleep
还没有执行完,需要等待 sleep
执行完,主线程从Event Queue读取task()
并执行。
所以有时setTimeout
并不能在所写的时间内完成,因为所指定的时间只是Event Table将回调函数加入到Event Queue的时间,如果函数为setTimeout(fn,0)
,也并不能即刻执行,这句话的意思是在主线程最早可得的空闲时间执行。即便主线程为空,也不能达到0ms,HTML标准规定最低为4ms。
setInterval
为每隔指定的时间就将注册的回调函数加入到Event Queue。
3、宏任务和微任务
除了将任务分为同步任务和异步任务,还可以将异步任务划分为宏任务和微任务:
- 宏任务:包括整体代码script、I/O、setTimeout、setInterval、requestAnimationFrame
- 微任务:包括Promise、process.nextTick(callback)
他们主要在于执行顺序不同,事件循环的走向不同。
不同类型的任务会进入不同的Event Queue,宏任务会进入相同的Event Queue,微任务会进入相同的Event Queue。
最终事件循环的顺序为:
- 首先进入整体代码,执行宏任务,开始第一次循环
- 第一轮宏任务执行完后,接着执行所有的微任务。
- 然后再从宏任务开始,找到一个任务队列,执行完毕,然后执行所有微任务。
setTimeout(function() {
console.log('setTimeout');
})
new Promise(function(resolve) {
console.log('promise');
}).then(function() {
console.log('then');
})
console.log('console');
- 整体代码作为宏任务开始执行,开始第一轮循环。
- 先遇到
setTimeout
,将其回调函数注册到Event Table,然后分发到宏任务Event Queue - 遇到
Promise
,new Promise
为实例化对象,是同步任务,立即执行,将异步任务回调函数then
分发到微任务Event Queue。 console.log
直接执行。- 此时第一个宏任务执行完毕,开始执行所有微任务,此时只有一个微任务
then
,执行then
。 - 第一轮事件循环结束,开始第二轮循环,此时有一个宏任务,即
setTimeout
的回调函数,从该函数开始执行。
4、async/await
async/await本质上是对Promise的封装,所以也属于微任务,与Promise的执行类似。
setTimeout(_ => console.log(4))
async function main() {
console.log(1)
await Promise.resolve()
console.log(3)
}
main()
console.log(2)
可以理解为await前面的代码是new Promise时传入的代码,await后面的代码为Promise.then中的回调。
来源:https://juejin.cn/post/6844903512845860872
来源:https://juejin.cn/post/6844903512845860872
五、执行上下文与执行栈
执行上下文
执行上下文就是评估和执行JavaScript代码的环境,JavaScript代码是在执行上下文中执行的。执行上下文分为:
- 全局执行上下文:是默认的基础的执行上下文,所有未在函数中的变量都在全局执行上下文中,一个程序只会有一个全局执行上下文。主要做了两件事:创建一个全局window对象(浏览器中),将this指向全局对象。
- 函数执行上下文:当函数被调用时创建,每个函数都有自己的函数执行上下文。
- eval函数执行上下文:在eval函数内部的代码有自己的执行上下文。
执行栈
一种LIFO后进先出的数据结构的栈,用来存储代码运行时创建的所有执行上下文。
当js引擎首次遇到代码时,会创建全局执行上下文并压入执行栈,当遇到函数调用时,会创建新的执行上下文并压入栈顶。当函数执行完毕后,会把他的执行上下文从栈顶弹出,到达下一个上下文。
执行上下文的创建
执行上下文的创建包含两个阶段:创建阶段和执行阶段
- 创建阶段:
- 完成this绑定:在全局执行上下文中,this指向全局对象(浏览器中指向window);函数执行上下文中,如果函数被一个引用对象调用,则指向该对象,否则指向全局对象或undifined(严格模式下)。
- 创建词法环境组件(作用域链)
- 创建变量环境组件
-
执行阶段
在这阶段,执行变量赋值、代码执行。如果Javascript引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配undefined值。 -
回收阶段
执行上下文出栈等待虚拟机回收执行上下文。
变量提升
变量提升(Hoisting)被认为是, Javascript中执行上下文 (特别是创建和执行阶段)工作方式的一种认识。我们可以理解成,在编译的阶段,js引擎帮我们把变量和函数的声明放在最前面,但实际上变量和函数声明在代码里的位置是不会动的。
1)创建阶段
用var关键字声明的变量是以默认值undefined来存储的。
用let,const声明的变量以uninitialized来存储的。这里就解释了,为什么我们用let和const声明的,不能在它之前使用,也就是暂时性死区。
2)执行阶段
由于函数是以对整个函数代码的引用来存储的,我们甚至可以在创建函数的那一行之前调用它们,也是能够正常的运行。
当我们引用一个在其声明前用var关键字声明的变量时,它将简单地返回其存储的默认值:undefined。
ES6中引出了const和let,就是为了防止意外地引用未定义的变量,就像我们用var关键字一样,每当我们试图访问未初始化的变量时,我们希望它会抛出一个ReferenceError。
https://juejin.cn/post/6844903607066689550
https://juejin.cn/post/6844903747617832973
来源:https://juejin.cn/post/6844903682283143181
https://juejin.cn/post/6844903607066689550
https://juejin.cn/post/6844903747617832973
https://juejin.cn/post/6933377315573497864
六、作用域、作用域链、原型、原型链
作用域
作用域是变量(变量作用域又称上下文)和函数生效(能被访问)的区域。作用域决定了代码区块中变量和其他资源的可见性。
一般把作用域分为:全局作用域、函数作用域、块级作用域。
a、全局作用域:任何不在函数中或大括号中声明的变量,都是在全局作用域下。全局作用域下声明的变量可以在程序的任意位置访问到。
b、函数作用域:如果一个变量在函数内部声明的,它就是在函数作用域下,这些变量只能在函数内部访问,不能再函数以外访问。
c、块级作用域:ES6引入了let和const关键字,在大括号中使用let和const声明的变量存在于块级作用域中,在大括号外不能被访问。
d、词法作用域又称静态作用域,即变量创建时就确定好了,而非执行阶段确定。
2)作用域链:当在Javascript中使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错。
作用域不销毁的情况:
- 主要作用:
- 收集和维护所有声明的标识符(变量或函数)
- 按
LHS
或RHS
来查找标识符 - 确定当前代码的访问权限
其中最重要的就是查询标识符。
查找分为两种:
LHS
:Left-hand Side。目的是为了赋值。
RHS
:Right-hand Side。目的是为了引用(或执行)。
作用域链
代码不仅可以访问当前作用域内的变量,还可以可以访问父级作用域内的变量,当多个作用域形成嵌套关系时,就出现了作用域链。
注意:当以RHS
的方式查找时,最终查找不到的话会抛出Uncaught ReferenceError
的错误,当按照LHS
的方式查找时,如果查找不到,会在查找最外层的作用域声明该变量,由此未声明的变量赋值后为全局变量。
原型
原型本质还是对象,可以将属性继承给实例。
构造函数通过.prototype
属性访问原型,通过new
创建实例;
通过instanceof
可以检查实例和构造函数的关系;
原型可以通过.constructor
属性访问构造函数;
实例可以通过_proto_
属性访问原型(隐式原型),通过._proto_.constructor
访问构造函数
原型链
在读取实例的某一个属性时,优先在该实例中查找该属性,如果没有找到,会循着._proto_
属性向原型上去查找,如果还找不到,就去尝试寻找原型的原型。这个搜索过程形成的链状关系就是原型链。所有函数的默认原型都是Object的实例。最上层为null。
注意:当以RHS
的方式查找时,最终查找不到的话会抛出undefined
,当按照LHS
的方式查找时,如果查找不到,则会在该实例中声明属性,而不会去查找原型链。
作用域链与原型链
作用域链的主要作用是查找标识符,当作用链需要查找标识符时会沿着作用域链依次查找,但原型链主要是为了查找引用类型的属性,当需要查找引用类型的属性时沿着原型链一次查找。
三、变量和类型
JavaScript数据类型
JavaScript数据类型分为原始类型和对象类型。
栈(stack):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
堆(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
1、缓存方式区别
1.栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放;
2.堆是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。
2、堆栈数据结构区别
原始类型
- 包括:
Null
、Undefined
、Boolean
、Number
、String
、Symbol
、Bigint
Null
:仅有一个值null
Undefined
:仅有一个值undefined
Boolean
:有true
和false
两个值
Number
:有整数和浮点数,还有NaN
、-Infinity
、+Infinity
特殊值
String
:一串字符序列
Symbol
:是唯一且不可改变的数据类型
Bigint
对象类型
- 包括:
Object
Object
:除了Object
,还包含特殊对象Array
、Function
存储
原始类型存储:栈存储
- 存储的值大小不可变
- 空间较小
- 可直接操作变量,效率高
- 系统自动分配存储空间
原始类型存储在栈中,变量定义时栈就为其分配好了内存空间,由于栈中内存空间的大小是固定的,就注定了存储在栈中的变量是不可变的。
不可变性:一旦创建就不可再改变,想要修改值需要在栈中开辟新内存保存新的内容,然后销毁原始值,再将变量指向这块空间。
对象类型存储:堆存储
- 存储的值大小不定,可动态调整
- 空间较大
- 不可直接操作,需要通过地址读取
- 通过代码进行分配空间
对象类型或引用类型的值在栈中只存储了一个固定长度的地址,这个地址指向堆中的内存。
复制
是对栈中数据进行复制,对于原始类型来说就是复制它的值,对于对象类型来说复制的是指向堆区的地址,所以两者指向的还是同一块内存。
比较
是对栈中数据进行比较,对于原始类型来说就是比较它的值,对于对象类型来说比较的是指向堆区的地址。
var name = 'ConardLi';
var name2 = 'ConardLi';
console.log(name === name2); // true
var obj = {name:'ConardLi'};
var obj2 = {name:'ConardLi'};
console.log(obj === obj2); // false
函数参数传递
ECMAScript中所有函数的参数都是按值传递的。
参数传递时,都会对参数复制出一个副本为局部变量,对于原始类型来说就是复制它的值,对于对象类型来说复制的是指向堆区的地址,指向的还是原来的堆区部分。
let obj = {};
function changeValue(obj){
obj.name = 'ConardLi';
obj = {name:'code秘密花园'};
}
changeValue(obj);
console.log(obj.name); // ConardLi
数据类型检测
Null:
表示值为空,是正常的,用于表示此处不应该有值。
转换为数值为0。
Undefined:
表示缺少值,是不正常的,表示该有值却未定义。
转换为数值为NaN。
Boolean:
Number:
String:
Symbol:
Array:
三、深拷贝与浅拷贝
主要针对对象类型
赋值:
- 对栈中数据进行复制,对象类型则复制的是堆区地址。
浅拷贝:
- 在堆区开辟新的内存存储,对原始对象属性值的精准拷贝,如果属性是原始类型拷贝该值,如果属性是对象类型拷贝其地址。所以属性值如果是对象类型则与原数据共享内存。
深拷贝:
- 在堆区开辟新的内存存储,将其从内存中完整的拷贝出来,如果属性是原始类型则拷贝该值,如果属性是对象类型则对对象类型的属性继续进行深拷贝。
由此可见,赋值在栈区进行简单的精准拷贝,浅拷贝在第一层属性进行简单的精准拷贝,深拷贝则是对对象的完整拷贝。
//赋值:
let aa = {
name : 'aa',
num : [1,[2,3],5]
};
let bb = aa;
bb.name = 'bb';
bb.num[0] = [2,3,4];//注意如果是bb.num=[2,3,4],会将该对象类型的属性重新赋值,由于栈区存放的地址相同,指向的对象没变,不管怎样修改都相当于对同一个对象进行修改,所以两者还是保持同步。
console.log(aa);//{name:'bb',num:[[2,3,4],[2,3],5]}
console.log(bb);//{name:'bb',num:[[2,3,4],[2,3],5]}
//浅拷贝:
let aa = {
name : 'aa',
num : [1,[2,3],5]
};
function shallowClone (obj) {
let cloneObj = {};
for ( let i in obj ) {
if ( obj.hasOwnProperty(i) ) {
cloneObj[i] = obj[i];
}
}
return cloneObj;
}
let bb = shallowClone(aa);
bb.name = 'bb';
bb.num[0] = [2,3,4];//console.log(bb.num===aa.num);为true
//注意如果是bb.num=[2,3,4],会将该属性重新赋值,此时地址改变,不再指向同一块地址,也不改变原属性的值。
//console.log(bb.num===aa.num);为false
console.log(aa);//{name:'aa',num:[[2,3,4],[2,3],5]}
console.log(bb);//{name:'bb',num:[[2,3,4],[2,3],5]}
//深拷贝
let aa = {
name : 'aa',
num : [1,[2,3],5]
};
function deepClone (obj) {
if (obj===null) return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (typeof obj!=='object') return obj;
let cloneObj = {};
for ( let i in obj ) {
if ( obj.hasOwnProperty(i) ) {
cloneObj[i] = deepClone(obj[i]);
}
}
return cloneObj;
}
let bb = deepClone(aa);
bb.name = 'bb';
bb.num[0] = [2,3,4];//console.log(bb.num===aa.num);为false
console.log(aa);//{name:'aa',num:[1,[2,3],5]}
console.log(bb);//{name:'bb',num:[[2,3,4],[2,3],5]}
-------------------------------------------Object.prototype.hasOwnProperty()
表示是否有自己的属性,会检查对象中是否有某个属性,如果没有也不会沿着原型链查找。
var obj = {
a: 1,
fn: function(){
},
c:{
d: 5
}
};
console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('fn')); // true
console.log(obj.hasOwnProperty('c')); // true
console.log(obj.c.hasOwnProperty('d')); // true
console.log(obj.hasOwnProperty('d')); // false, obj对象没有d属性
console.log(obj.hasOwnProperty("hasOwnProperty"));//false 继承自Object原型上的方法
var str = new String();
// split方法是String这个对象的方法,str对象本身是没有这个split这个属性的
console.log(str.hasOwnProperty('split')); // false
console.log(String.prototype.hasOwnProperty('split')); // true
原型中的属性不算。
var o={name:'jim'};
function Person(){
this.age=19;
}
Person.prototype=o;//修改Person的原型指向
p.hasOwnProperty("name");//false 无法判断继承的name属性
p.hasOwnProperty("age");//true;
与for in
不同,会忽略从原型链上继承的属性。
var o={
gender:'男'
}
function Person(){
this.name="张三";
this.age=19;
}
Person.prototype=o;
var p = new Person();
for(var k in p){
if(p.hasOwnProperty(k)){
console.log("自身属性:"+k);// name ,age
}else{
console.log("继承的属性:"+k);// gender
}
}
可以被重写覆盖
var o={
gender:'男',
hasOwnProperty:function(){
return false;
}
}
o.hasOwnProperty("gender");//不关写什么都会返回false
//解决方式,利用call方法
({}).hasOwnProperty.call(o,'gender');//true
Object.prototype.hasOwnProperty.call(o,'gender');//true
-------------------------------------------Object.prototype.isPrototypeOf()
测试某方法是否在另一个对象的原型链上。
var o={};
function Person(){};
var p1 =new Person();//继承自原来的原型,但是现在已经无法访问
Person.prototype=o;
var p2 =new Person();//继承自o
console.log(o.isPrototypeOf(p1));//false o是不是p1的原型
console.log(o.isPrototypeof(p2));//true o是不是p2的原型
console.log(Object.prototype.isPrototypeOf(p1));//true
console.log(Object.prototype.isPrototypeOf(p2));//true
浅拷贝的实现方式:
1、Object.assign()
Object.assign()可以将原对象的可枚举属性拷贝给目标对象,然后返回目标对象。
let aa = {
name : 'aa',
num : [1,[2,3],5]
};
let bb = Object.assign({},aa);
bb.name = 'bb';
bb.num[0] = [2,3,4];
console.log(aa);//{"name": "aa","num": [[2,3,4],[2,3], 5]}
2、lodash的clone方法
let _ = require('lodash');
let aa = {
name : 'aa',
num : [1,[2,3],5]
};
let bb = _.clone(aa);
bb.name = 'bb';
console.log(aa.num===bb.num);//true
3、展开运算符
let aa = {
name : 'aa',
num : [1,[2,3],5]
};
let bb = {...aa};
bb.name = 'bb';
console.log(aa.num===bb.num);//true
4、利用Array.prototype.concat()
但只能用于数组:
let aa = [1, { name: "aa", num: [1, [2, 3], 5] }];
let bb = aa.concat();
bb[1].name = "bb";
console.log(aa[1].num === bb[1].num); //true
console.log(bb);
console.log(aa);
用于对象时会将该对象转换为数组第一个元素
let aa = {
name : 'aa',
num : [1,[2,3],5]
};
let bb = Array.prototype.concat.call(aa);
bb.name = 'bb';
console.log(aa.num===bb.num);//false
console.log(aa);
console.log(bb);
5、Array.prototype.slice()
只能用于数组
let aa = [1, { name: "aa", num: [1, [2, 3], 5] }];
let bb = aa.slice();
bb[1].name = "bb";
console.log(aa[1].num === bb[1].num); //true
console.log(aa[1].num);
console.log(aa);
console.log(bb);
用于对象时会将其删掉
let aa = {
name : 'aa',
num : [1,[2,3],5]
};
let bb = Array.prototype.slice.call(aa);
bb.name = 'bb';
console.log(aa.num===bb.num);//false
console.log(aa);
console.log(bb);
深拷贝的实现方式:
1、JSON.parse(JSON.stringify())
可用于数组或对象,相当于将对象先转换为字符串,然后再解析成新对象,但是如果其中有函数和正则,函数会变为null,正则会变为空对象。
let aa = [1,
{name : 'aa',
num : [1,[2,3],5]},
{function(){}}
];
let bb = JSON.parse(JSON.stringify(aa));
console.log(aa[1].num===bb[1].num);//false
aa[1].num[0] = [1,2,3];
console.log(aa);
console.log(bb);
2、lodash的cloneDeep方法
let _ = require('lodash');
let aa = [1,
{name : 'aa',
num : [1,[2,3],5]},
];
let bb = _.cloneDeep(aa);
console.log(aa[1].num===bb[1].num);//false
3、jQuery.extend()
可用于对象
let $ = require('jquery');
let aa =
{name : 'aa',
num : [1,[2,3],5],
fun: function(){}};
let bb = $.extend(true,{},aa);
console.log(aa.num===bb.num);//false
aa.num[0] = [1,2,3];
console.log(aa);
console.log(bb);
用于数组时会发生转换
let $ = require('jquery');
let aa = [1,
{name : 'aa',
num : [1,[2,3],5]},
{function(){}}
];
let bb = $.extend(true,{},aa);
console.log(aa[1].num===bb[1].num);//false
aa[1].num[0] = [1,2,3];
console.log(aa);
console.log(bb);
4、手写递归方法
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);
四、懒加载与预加载
1、懒加载:
延迟加载甚至不加载,是优化网页性能的方式。
适用于长网页,仅对可视区域的资源进行加载。
方法:
对于图片,先将src置空,设置属性lazyload为true,添加属性data-origin,填入图片的URL,对scroll事件进行监听,当图片到达可视区域时将src内容换为data-origin内容即可。
优缺点:
- 提升用户体验,避免长时间等待;
- 减少加载不需要的资源,降低服务器压力和流量
- 防止并发加载资源过多造成阻塞。
2、预加载:
提前加载,也是优化网页性能的方式。对可能使用到的资源进行提前请求加载到本地,当需要使用时就从缓存中加载。
- 方法:
- 利用HTML标签预加载;
<img src="http://pic26.nipic.com/20121213/6168183 0044449030002.jpg" style="display:none"/>
- 利用Image对象。
<script src="./myPreload.js"></script>
//myPreload.js文件
var image= new Image()
image.src="http://pic26.nipic.com/20121213/6168183 004444903000 2.jpg"
- 优缺点:
用户体验好,等待时间少
增加服务器压力
两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。
懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
来源:https://juejin.cn/post/6844903797039300615
来源:https://juejin.cn/post/6844903485591257102
七、闭包
JavasSript 语言的特别之处就在于:函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量。
闭包其实就是一个可以访问其他函数内部变量的函数。创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以 访问到当前函数的局部变量。闭包的本质是当前环境中存在指向父级作用域的引用。
因为通常情况下,函数内部变量是无法在外部访问的(即全局变量和局部变量的区别),因此使用闭包的作用,就具备实现了能在外部访问某个函数内部变量的功能,让这些内部变量的值始终可以保存在内存中。
缺点:
-
a、闭包会使得函数中的变量都被保存在内存中,内存消耗很大,可能导致内存泄露。在不使用闭包时,把被引用的变量设置为null,即手动清除变量。
-
b、闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
1)闭包常用的用途
a、闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
b、函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,延长声明周期。因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
数据存放的正确规则是:局部、占用空间确定的数据,一般会存放在栈中,否则就在堆中(也有例外)。
上图中画红框的位置我们能看到一个内部的对象[[Scopes]],其中存放着变量 a,该对象是被存放在堆上的,其中包含了闭包、全局对象等等内容,因此我们能通过闭包访问到本该销毁的变量。
其余见链接
- 一个函数即使在它的词法作用域之外被调用时仍能记住并访问其所在的词法作用域。
- 能够读取其他函数内部变量,是函数内部和函数外部沟通的桥梁。
- 一个函数在创建时就与它的词法环境绑定在一起构成闭包。
在实践中,尤其是指当声明该函数的上层上下文已经被销毁时,函数仍能访问其上层上下文的作用域链。正常函数调用后,作用域链被销毁,但在存在闭包时,作用域链会被保存。作用域链是单向的,从内到外,由下至上,作用域链中会保存全局变量、局部变量、函数参数。
主要应用于:
- 封装私有变量,在函数外部访问函数内部变量,用户无法直接获取或修改变量的值,只能通过调用函数。
- 回调与计时器
- 即时函数:独立作用域
注意点:
- 闭包会使得函数中的变量都保存在内存中,如果滥用闭包会导致网页性能问题甚至导致内存泄漏。此时再退出函数之前应该删除不使用的局部变量。
内存泄露
- 同一个上下文创建的闭包共用一个该上层上下文。一个函数内的所有函数共享该上层上下文。可以用立即执行匿名函数来隔离作用域。
- 当把闭包当作函数的公有方法,把内部变量当作私有属性时,不要随意改变父函数内内部变量的值,否则可能会出问题。
https://juejin.cn/post/6947841638118998029
https://juejin.cn/post/6984188410659340324来源:https://yuguang.blog.csdn.net/article/details/106940646
来源:https://juejin.cn/post/6844903815041253390
八、this、apply、call、bind
this
this永远指向最后调用它的对象,在调用时才能确定。当在该对象内无法找到属性或方法时也不会继续沿着作用域链向上寻找,而是输出undefined。
函数调用方式来判断this:
- 作为一个函数调用
此时是最简单的函数,非严格模式下属于全局对象(window),严格模式下this为undefined。 - 作为一个方法调用
此时this指向最后调用他的对象。 - 使用构造函数调用函数
通过new来创建对象的过程:
首先新建了一个空对象
然后将该空对象的隐式原型指向其构造函数的显式原型
接着用call来改变this的指向
最后如果无返回值或者返回一个非对象值,则将 obj 返回作为新对象;如果返回值是一个新对象的话那么直接返回该对象。
整个过程通过call改变了this指向。
// 构造函数:
function myFunction(arg1, arg2) {
this.firstName = arg1;
this.lastName = arg2;
}
// This creates a new object
var a = new myFunction("Li","Cherry");
a.lastName; // 返回 "Cherry"
//new的过程:
var a = new myFunction("Li","Cherry");
new myFunction{
var obj = {};
obj.__proto__ = myFunction.prototype;
var result = myFunction.call(obj,"Li","Cherry");
return typeof result === 'obj'? result : obj;
}
- 作为函数方法调用函数(call、apply)
分析下面的例子中this的指向:
var name = "windowsName";
var a = {
name : "Cherry",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
},100);
}
};
a.func2() // this.func1 is not a function
由于setTimeout
是挂载在window
上的,是window
的一个方法,所以this
改为指向window
,而window
中没有func1
函数,所以报错了。注意:自执行匿名函数的this
会指向window
。
改变this指向的方法:
1、用箭头函数:
箭头函数的this
在函数定义时确定,需要按照作用域链来进行确定,绑定的是最近一层非箭头函数的this
,否则的话为undefined
。
2、函数内部用_this=this:
将调用函数的对象进行保存,使用时用保存的变量进行调用。
3、 用call、apply、bind:
fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)
thisArg
:fun
运行时指定的参数的值。
- 非严格模式下如果传入
null
或者undefined
则自动指向全局对象(window
)。严格模式下为undefined
。 - 如果传入原始值(数字、字符串、布尔值),
this
会自动指向该原始值的自动包装对象,如 String、Number、Boolean。
调用他们的必须是函数,他们都能改变this
的指向。
apply
和call
唯一的不同就是传入参数不同,apply
接受的是数组或类数组对象,call
接受的是一系列参数。参数数量、顺序确定或参数量不大时用call
,不确定或参数量大时用apply
。
apply
和call
返回的都是函数执行结果,都是立即执行,而bind
返回函数的拷贝,不立即执行需要手动进行调用。
比如:
Object.prototype.toString.call(data)
var a ={
name : "Cherry",
fn : function (a,b) {
console.log( a + b)
}
}
var b = a.fn;
b.bind(a,1,2)() // 3
call/apply/bind
的核心理念:借用方法。借助已实现的方法,改变方法中数据的this
指向,减少重复代码,节省内存。
来源:https://juejin.cn/post/6844903496253177863
来源:https://juejin.cn/post/6844903906279964686
九、类和继承
类
-------------------------------------------Object.create()
Object.create(proto,[propertiesObject])
通过原型创建对象,可添加某些属性。
proto
:创建对象的原型对象
propertiesObject
:可选。增加到新对象的可枚举属性。必须是对象!
注意:增加的属性是其自身的属性,而不是增加到原型链上。
const apple = {
shape:'round',
taste: function() {console.log(`${this.color} apple is ${this.t},it is ${this.price} yuan!`);
}
//注意!!!!!!!!!!以下:
// taste:`${this.color} apple is ${this.t},it is ${this.price} yuan!`
//然后再在全局打印是不正确的,因为在全局调用时,this变成了window,此时这些属性都不存在,变为undefined。
};
const greenapple = Object.create(apple,{
color:{
value:'red'},
t:{
value:'sweet',
writable:true
}
});
greenapple.shape='not round';
greenapple.price=15;
greenapple.taste();
console.log(greenapple.taste);//结果是ƒ () {
// console.log(
// `${this.color} apple is ${this.t},it is ${this.price} yuan!`
// );
//}
console.log(greenapple.taste());//结果会是red apple is sweet,it is 15 yuan!
//05-exercise.html:93 undefined
//因为先执行了内部的打印,但没有返回值,所以打印undefined
1、{}
var o = {a:1};
console.log(o)
打印如下:
继承了Object
的很多方法,如hasOwnProperty
、toString
等。
2、Object.create(null)
var o = Object.create(null,{
a:{
writable:true,
configurable:true,
value:'1'
}
})
console.log(o)
打印如下:
可以看到,没有继承任何方法,因为原型链上没有任何属性方法。如果o.toString()会报错。
3、Object.create(Object.prototype)
var o = Object.create(Object.prototype,{
a:{
writable:true,
configurable:true,
value:'1'
}
})
console.log(o)
打印如下:
与{}创建的一样,可见{}实际类似于创建原型Object.prototype的对象。
4、Object.create({})
var o = Object.create({},{
a:{
writable:true,
configurable:true,
value:'1'
}
})
console.log(o)
打印如下:
多了一层proto
嵌套。
5、总结
- 由
Object.create(null)
创建的对象没有继承任何属性,所以非常纯净,可以定义自己的方法,不用担心覆盖原型链上的同名方法。可用作数据字典。 - 可以避免
for...in
循环时遍历到对象原型链上的属性,使用create(null)
就可以不用进行hasOwnProperty
检查,可以减少性能损失。
八种继承方式:
1、原型链继承
function A() {
this.AA = 'AAvalue';
this.Acolor = ['red','yellow'];
}
A.prototype.Amethod = function() {
return this.AA;
};
A.prototype.Anum = [1,2,3];
function B() {
this.BB = 'BBvalue';
this.Bcolor = ['blue','green'];
}
B.prototype = new A();
B.prototype.Bmethod = function() {
return this.BB;
};
B.prototype.Bnum = [4,5,6];
let zz = new B();
console.log(zz.Amethod());
// alert(zz.AA);
console.log(zz.Bmethod());
// alert(zz.BB);
//测试对引用类型的修改
zz.Acolor.push('black');
zz.Anum.push(9),
zz.Bcolor.push('white');
zz.Bnum.push(8);
console.log(zz.Acolor);
console.log(zz.Bcolor);
let yy = new B();
console.log(yy.Acolor);
console.log(yy.Anum);
console.log(yy.Bcolor);
console.log(yy.Bnum);
引用对象会被修改的区域:A的构造函数中、A的原型中、B的原型中。
引用对象不会被修改的区域:B的构造函数中。
特点:
- 能继承构造函数和原型中的属性和方法。
- 但多个实例对引用类型的操作会被篡改。
2、借用构造函数继承
function A() {
this.AA = 'AAvalue';
this.Acolor = ['red','yellow'];
}
function B() {
A.call(this);
this.BB = 'BBvalue';
this.Bcolor = ['blue','green'];
}
let zz = new B();
console.log(zz.AA);
console.log(zz.BB);
//测试对引用类型的修改
zz.Acolor.push('black');
zz.Bcolor.push('white');
console.log(zz.Acolor);
console.log(zz.Bcolor);
let yy = new B();
console.log(yy.Acolor);
console.log(yy.Bcolor);
特点:
- 但多个实例对引用类型的操作不会被篡改。
- 只能继承构造函数中的属性和方法,不能继承原型中的属性和方法。
- 每个实例都会将构造函数中的属性复制一份,影响性能。
3、组合继承
function A() {
this.AA = 'AAvalue';
this.Acolor = ['red','yellow'];
}
A.prototype.Amethod = function() {
return this.AA;
};
A.prototype.Anum = [1,2,3];
function B() {
A.call(this);
this.BB = 'BBvalue';
this.Bcolor = ['blue','green'];
}
B.prototype = new A();
B.prototype.constructor = B;
B.prototype.Bmethod = function() {
return this.BB;
};
B.prototype.Bnum = [4,5,6];
let zz = new B();
console.log(zz.Amethod());
// alert(zz.AA);
console.log(zz.Bmethod());
// alert(zz.BB);
//测试对引用类型的修改
zz.Acolor.push('black');
zz.Anum.push(9),
zz.Bcolor.push('white');
zz.Bnum.push(8);
console.log(zz.Acolor);
console.log(zz.Anum);
console.log(zz.Bcolor);
console.log(zz.Bnum);
let yy = new B();
console.log(yy.Acolor);
console.log(yy.Anum);
console.log(yy.Bcolor);
console.log(yy.Bnum);
从原型继承的AA、Acolor在实例化时被从构造函数继承的AA、Acolor覆盖,由此多实例修改时不会造成变化,每次都是新建的。
特点:
- 从A的构造函数继承了两次其中的属性方法,所以存在两份相同的属性方法。
4、原型式继承
function createH(obj) {
function H(){}
H.prototype = obj;
return new H();
}
let obj = {objvalue:123};
let h1 = createH(obj);
console.log(h1);
//相当于Object.create();
let h2 = Object.create(obj);
console.log(h2);
此方法类似于浅拷贝。
特点:
- 可以得到来自原型的属性和方法。
- 也存在引用类型被篡改的可能。
5、寄生式继承
function createH(obj) {
function H(){}
H.prototype = obj;
return new H();
}
function createfullH(obj){
var hh = createH(obj);
hh.hmethod = function(){};
return hh;
}
let obj = {objvalue:123};
let h1 = createfullH(obj);
console.log(h1);
特点:
- 与原型式继承类似,相当于给继承函数增加了属性方法。
6、寄生组合式继承
function createRelation(A,B) {
let Bprototype = Object.create(A.prototype);
B.prototype = Bprototype;
B.prototype.constructor = B;
}
function A() {
this.AA = 'AAvalue';
this.Acolor = ['red','yellow'];
}
A.prototype.Amethod = function() {
return this.AA;
};
A.prototype.Anum = [1,2,3];
function B() {
A.call(this);
this.BB = 'BBvalue';
this.Bcolor = ['blue','green'];
}
createRelation(A,B);
B.prototype.Bmethod = function() {
return this.BB;
};
B.prototype.Bnum = [4,5,6];
let zz = new B();
console.log(zz.Amethod());
// alert(zz.AA);
console.log(zz.Bmethod());
// alert(zz.BB);
//测试对引用类型的修改
zz.Acolor.push('black');
zz.Anum.push(9),
zz.Bcolor.push('white');
zz.Bnum.push(8);
console.log(zz.Acolor);
console.log(zz.Anum);
console.log(zz.Bcolor);
console.log(zz.Bnum);
let yy = new B();
console.log(yy.Acolor);
console.log(yy.Anum);
console.log(yy.Bcolor);
console.log(yy.Bnum);
此时输出与方式3组合式继承相同,但此时实例中只有一组AA、Acolor。
7、混入方式继承多个对象
function A() {
this.AA = 'Avalue';
}
function B() {
this.BB = 'Bvalue';
}
function C() {
A.call(this);
B.call(this);
this.CC = 'Cvalue';
}
A.prototype.Amethod = function(){};
B.prototype.Bmethod = function(){};
C.prototype = Object.create(A.prototype);
Object.assign(C.prototype,B.prototype);
C.prototype.constructor = C;
C.prototype.Cmethod = function(){};
let zz = new C();
console.log(zz);
Object.assign
会把B
原型上的方法属性拷贝到C
原型上,使C
的所有实例都可用B
的方法。
8、ES6类继承extends
class A{
constructor(m,n){
this.Avalue = m;
this.Anum = n;
};
get Aresult(){
return Acal();
};
Acal(){
return this.Avalue+this.Anum;
};
}
class B extends A{
constructor(m,n,p,q){
super(m,n);
this.Bvalue = p;
this.Anum = q;
};
get Bresult(){
return Bcal();
};
Bcal(){
return this.Bvalue+this.Bnum;
};
}
const b = new B(1,2,3,4);
console.log(b);
十一、异步回调地狱
promise、generator、async/await
十二、事件流
事件冒泡:是从最具体的元素开始出发,然后开始向上传播至没那么具体的元素。
事件捕获:是从最不具体的节点捕获,最具体的节点最后收到事件。
事件流:
事件流是指页面接收事件的顺序,事件流三个阶段:事件捕获、到达目标、事件冒泡。
- 事件捕获阶段最先发生,可以提前拦截事件。
- 事件冒泡阶段最后发生,最迟在这个阶段响应事件。
- 一般认为捕获阶段未命中事件目标,事件处理被认为是冒泡阶段的一部分,但目前现代浏览器都会在捕获阶段上在事件目标触发事件。最终导致事件目标上有两个机会可以处理事件。
https://juejin.cn/post/6844903834075021326
https://github.com/amandakelake/blog/issues/38
十三、垃圾回收机制
垃圾回收机制:
由于数组、对象、字符串等都是大小不定的,所以每次都需要解释器在堆区动态分配内存,不再使用时也需要进行释放才能再次使用。垃圾回收工作在JavaScript引擎内部,负责找出内存中的垃圾并回收再利用。
堆:动态存放对象的内存空间
mutator:需要大量内存
allocator:mutator将需要内存的申请提交到此,allocator负责从堆中调取内存
活动对象:mutator引用的对象
引用计数法
语言引擎存在一个引用表,记录每个对象自己被引用的次数,当被引用次数为0时立即进行回收,把内存作为空闲空间回收到空闲链表。
缺点:
- 计数器需要占用一定空间
- 出现循环引用时无法解决回收问题
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2,o2的引用次数是1
o2.a = o; // o2 引用 o,o的引用此时是1
return "azerty";
}
f();
按理说函数执行完作用域内空间应该回收,但由于o、o2被引用次数都为1,所以不可回收。
优点:
- 可以即刻回收垃圾
- 即时回收,所以用于GC的单独暂停时间短,最大暂停时间短。
- 不需要进行遍历所有活动对象和非活动对象
标记清除法
给所有被引用的活动对象加上标记,清除所有未被标记的非活动对象。
标记阶段:
从根(全局作用域)开始进行逐层往里遍历(深度遍历),被遍历到的对象即被引用的对象,打上标记,直到遍历到最深的节点。
清除阶段:
遍历整个堆,回收没有被标记的对象。
缺点:
- 每次都要进行遍历
- 造成碎片化
优点:
- 实现简单,只需要看有没有打标记
- 能解决循环引用问题。
标记-清除法是定时运行的,程序运行一段时间后统一GC。
复制法
把内存分为两部分,一部分是From空间,一部分是To空间。把所有活动对象从From空间复制到To空间,释放整个From空间,然后把两者互换身份,继续进行。
内存泄漏的情况:
堆区中已动态分配的的内存无法被回收或未被回收,造成系统资源浪费,程序运行缓慢甚至系统崩溃。
如何避免内存泄漏:
立即执行匿名函数
promise
九、Symbol
如何让事件先冒泡后捕获
参考回答:
在DOM标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件,先暂缓执行,直到冒泡事件被捕获后再执行捕获之间。
事件委托
参考回答:
简介:事件委托指的是,不在事件的发生地(直接dom)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生元素DOM的类型,来做出不同的响应。
举例:最经典的就是ul和li标签的事件监听,比如我们在添加事件时候,采用事件委托机制,不会在li标签上直接添加,而是在ul父元素上添加。
好处:比较合适动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制。
mouseover和mouseenter的区别
参考回答:
mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是mouseout
mouseenter:当鼠标移除元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是mouseleave
js的各种位置,比如clientHeight,scrollHeight,offsetHeight ,以及scrollTop, offsetTop,clientTop的区别?
参考回答:
clientHeight:表示的是可视区域的高度,不包含border和滚动条
offsetHeight:表示可视区域的高度,包含了border和滚动条
scrollHeight:表示了所有区域的高度,包含了因为滚动被隐藏的部分。
clientTop:表示边框border的厚度,在未指定的情况下一般为0
scrollTop:滚动后被隐藏的高度,获取对象相对于由offsetParent属性指定的父坐标(css定位的元素或body元素)距离顶端的高度。
js拖拽功能的实现
参考回答:
首先是三个事件,分别是mousedown,mousemove,mouseup
当鼠标点击按下的时候,需要一个tag标识此时已经按下,可以执行mousemove里面的具体方法。
clientX,clientY标识的是鼠标的坐标,分别标识横坐标和纵坐标,并且我们用offsetX和offsetY来表示元素的元素的初始坐标,移动的举例应该是:
鼠标移动时候的坐标-鼠标按下去时候的坐标。
也就是说定位信息为:
鼠标移动时候的坐标-鼠标按下去时候的坐标+元素初始情况下的offetLeft.
还有一点也是原理性的东西,也就是拖拽的同时是绝对定位,我们改变的是绝对定位条件下的left
以及top等等值。
补充:也可以通过html5的拖放(Drag 和 drop)来实现
● 异步加载js的方法
参考回答:
defer:只支持IE如果您的脚本不会改变文档的内容,可将 defer 属性加入到
Ajax解决浏览器缓存问题
参考回答:
在ajax发送请求前加上 anyAjaxObj.setRequestHeader(“If-Modified-Since”,“0”)。
在ajax发送请求前加上 anyAjaxObj.setRequestHeader(“Cache-Control”,“no-cache”)。
在URL后面加上一个随机数: “fresh=” + Math.random()。
在URL后面加上时间搓:“nowtime=” + new Date().getTime()。
如果是使用jQuery,直接这样就可以了 $.ajaxSetup({cache:false})。这样页面的所有ajax都会执行这条语句就是不需要保存缓存记录。
将原生的ajax封装成promise
参考回答:
复制代码
var myNewAjax=function(url){
return new Promise(function(resolve,reject){
var xhr = new XMLHttpRequest();
xhr.open(‘get’,url);
xhr.send(data);
xhr.onreadystatechange=function(){
if(xhr.status200&&readyState4){
var json=JSON.parse(xhr.responseText);
resolve(json)
}else if(xhr.readyState==4&&xhr.status!=200){
reject(‘error’);
}
}
})
}
js监听对象属性的改变
参考回答:
我们假设这里有一个user对象,
(1)在ES5中可以通过Object.defineProperty来实现已有属性的监听
Object.defineProperty(user,‘name’,{
set:function(key,value){
}
})
缺点:如果id不在user对象中,则不能监听id的变化
(2)在ES6中可以通过Proxy来实现
var user = new Proxy({},{
set:function(target,key,value,receiver){
}
})
这样即使有属性在user中不存在,通过user.id来定义也同样可以这样监听这个属性的变化哦~
JavaScript数组操作方法之concat()与slice()、splice()
hash.set hash.get javascript