目录
1.闭包
概念:闭包就是能够读取其他函数内部变量的函数,能够实现函数外部访问到函数内部的变量。(JS中内层函数可以访问外层函数的变量,外层函数无法操作内层函数的变量)
简单理解:“函数”和“函数内部能访问到的变量”的总和,就是一个闭包。
优点:可以重复利用变量,并且该变量不会污染全局;隔离作用域,保护私有变量;让函数外部可以操作(读写)函数内部的数据(变量/函数);变量长期驻扎在内存中,不会被内存回收机制回收,即延长变量的生命周期。
注意:函数执行完后,函数内部声明的局部变量一般不存在,但闭包中的变量可能存在。在函数外部不能直接访问函数内部的局部变量,但是可以通过闭包让外部操作它。
缺点:闭包较多时,会消耗内存,导致页面性能下降。如果不释放内存,会引起内存泄漏。
function fn1() {
let arr = new Array(100)
fnction fn2() {
lconsole.log(arr.length)
}
return fn2
}
let f = fn1()
f()
f = null // 让内部函数成为垃圾对象,回收闭包
// 也可以调用时直接f()()运行即可,匿名函数用完自动销毁
闭包的生命周期:
- 产生:在嵌套内部函数定义执行完时就产生了(不是调用)。
- 死亡:在嵌套的内部函数成为垃圾对象时。
使用场景:防抖,节流,函数嵌套函数避免全局污染的时候。
笔试题:
// 补全add函数,输出对应结果
function add(n) {
//TODO
}
add(1)(2)() // 3
add(1)(2)(3)(4)() // 10
//答案
function add(n) {
if(!n) { return res }
res = n
return function(n) {
return add(res + n)
}
}
console.log(add(1)(2)(3)()) // 6
console.log(add(1)(2)(3)(4)()) // 10
2.原型链
(1)原型对象:每一个函数在创建时都会被赋予一个prototype属性,它指向函数的原型对象,这个对象包含所有实例共享的属性和函数。
(2)原型链:对象的每个实例都具有一个__proto__属性,指向构造函数的原型对象。而原型对象同样存在一个__proto__属性指向上一级构造函数的原型对象,就这样层层往上,直到最上层某个原型对象为null。
注意:原型链上的所有节点都是对象,不能是字符串、数字、布尔值等原始类型。要求原型链必须是有限长度的。由于无法访问null的属性,起到了终止原型链的作用。
3.作用域链
(1)作用域:在运行代码时某些特定部分的变量、函数和对象的可访问性。作用域规定了如何设置变量,也就是确定当前执行代码对变量的访问权限。JavaScript采用词法作用域,也就是静态作用域,即在函数定义的时候就已经确定了。
(2)变量对象:变量对象是当前代码段中,所有的变量(变量,函数,形参,arguments)组成的一个对象。变量对象是在执行上下文中被激活的,只有变量对象被激活了,在这段代码中才能使用所有的变量。变量对象分为全局变量对象和局部变量对象。全局简称为Variab Object VO,函数由于执行才被激活,成为Active Object AO。
(3)作用域链:在js中,函数存在一个隐式属性[scopes](域),这个属性用来保存当前函数的执行上下文环境。由于在数据结构上是链式的,因此也被称作作用域链。可以将它理解为一个数组,一系列的AO对象所组成的一个链式结构。
JavaScript关于作用域、作用域链和闭包的理解-CSDN博客
4.防抖、节流
5.常用数组方法
(1)push():向数组的末尾添加一个或多个元素,并返回新的数组长度。原数组改变。
let arr = [1, 2, 3, 4]
arr.push(5, 6, 7)
arr = [1, 2, 3, 4, 5, 6, 7]
(2)pop():删除并返回数组的最后一个元素,若该数组为空,则返回undefined。原数组改变。
let arr = [1, 2, 3, 4, 5, 6, 7]
let del = arr.pop()
// del = 7
// arr = [1, 2, 3, 4, 5, 6]
(3)unshift():向数组的开头添加一个或多个元素,并返回新的数组长度。原数组改变。
let arr = [1, 2, 3, 4, 5, 6, 7]
let res = arr.unshift(0)
// res = 8
// arr = [0, 1, 2, 3, 4, 5, 6, 7]
(4)shift():删除数组的第一项,并返回该元素的值。若该数组为空,则返回undefined。原数组改变。
let arr = [1, 2, 3, 4, 5, 6, 7]
let res = arr.shift()
// res = 1
// arr = [2, 3, 4, 5, 6, 7]
(5)concat(arr1,arr2,...):合并两个或多个数组,生成一个新的数组。原数组不变。
let arr = [1, 2, 3, 4, 5, 6, 7]
let arr1 = ["a", "b", "c"]
let arr2 = ["x", "y", "z"]
let res = arr.concat(arr1, arr2)
// res = [1, 2, 3, 4, 5, 6, 7, "a", "b", "c", "x", "y", "z"]
// arr = [1, 2, 3, 4, 5, 6, 7]
(6)join():将数组的每一项用指定字符连接形成一个字符串,默认连接字符为逗号。
let arr = [1, 2, 3, 4, 5, 6, 7]
let str1 = arr.join()
let str2 = arr.join("-")
// str1 = 1,2,3,4,5,6,7
// str2 = 1-2-3-4-5-6-7
(7)reverse():将数组倒序。原数组改变。
let arr = [1, 2, 3, 4, 5, 6, 7]
arr.reverse()
// arr = [7, 6, 5, 4, 3, 2, 1]
(8)sort():对数组元素进行排序,按照字符串UniCode码排序。原数组改变。
// 从小到大排序
let arr = [1, 4, 3, 55, 12, 5]
let sortNum = function(a, b) {
return a-b
}
arr.sort(sortNum) // [1, 3, 4, 5, 12, 55]
// 从大到小排序
let arr = [1, 4, 3, 55, 12, 5]
let sortNum = function(a, b) {
return b-a
}
arr.sort(sortNum) // [55, 12, 5, 4, 3, 1]
// 按照数组对象中的某个值排序
let arr = [
{name:"张三", age:18},
{name:"李四", age:16},
{name:"王五", age:20}
]
function compare(param) {
return function sortAge(a, b) {
return a[param]-b[param]
}
}
arr.sort(compare("age")) // [{name:"李四", age:16},{name:"张三", age:18},{name:"王五", age:20}]
(9)map(function):接收一个函数作为参数,返回一个新的数组。原数组不变。(注意和forEach的区别)。
let arr = [1, 2, 3, 4, 5]
let arr1 = arr.map(item => item = 2)
// arr = [1, 2, 3, 4, 5]
// arr1 = [2, 2, 2, 2, 2]
(10)slice():按照条件截取出其中的部分内容。
- array.slice(n, m):从索引n开始查找到索引m处(不包含m)。
- array.slice(n):从索引n开始查找到末尾。
- array.slice(0):原样输出数组,可以实现数组克隆。
- array.slice(-n, -m):从最后一项开始算起,-1为最后一项,-2为倒数第二项,返回一个新数组。原数组不变。
(11)splice(index, n, arr1, arr2...):用于添加或删除数组中的元素。从index位置开始删除n个元素,并将arr1、arr2...数据从index位置依次插入。n为0时,不删除元素。原数组改变。
let fruits = ["Banana", "Orange", "Apple", "Mango"]
fruits.splice(2, 1, "Lemon", "Kiwi")
// fruits = ["Banana", "Orange", "Lemon", "Kiwi", "Mango"]
(12)forEach(function):为每个数组元素调用一次函数(回调函数)。原数组不变。(注意和map的区别,若直接打印Array.forEach,结果为undefined)。
(13)filter(function):过滤数组中符合条件的元素并返回一个新的数组。
let arr = [1, 2, 3, 4, 5]
let arr1 = arr.filter(x => x>3)
// arr1 = [4, 5]
(14)every(function):对数组中的每一项进行判断,若都符合则返回true。否则返回false。
(15)some(function):对数组中的每一项进行判断,若都不符合则返回false。否则返回true。
(16)reduce(function):在每个数组元素上(从左到右)运行函数,以生成单个值。
let arr = [1, 2, 3, 4, 5]
let total = arr.reduce((a, b) => a+b)
// total = 15
(17)indexOf(item,index):检测当前值在数组中第一次出现的位置索引。item为查找的元素,index为字符串中开始检索的位置,返回第一次查到的索引,未找到返回-1。原数组不变。
(18)includes():判断数组是否包含一个指定的值。原数组不变。
改变原数组的方法:push,pop,shift,unshift,reverse,sort,splice
不改变原数组的方法:concat,map,filter,join,every,some,indexOf,slice,forEach
slice和splice的区别:splice改变原数组,slice不改变原数组。splice除了删除之外,还可以插入。splice可传入3个参数,slice接受2个参数。
map和forEach的区别:forEach()方法不会返回执行结果,而是undefined,map()方法会得到一个新的数组并返回。同样的一组数组,map()的执行速度优于forEach()。forEach()适用于并不打算改变数据的时候,而只想用数据做一些事情(比如存入数据库),map()适用于要改变数据值的时候,它更快,并且返回一个新的数组。
JavaScript中的数组方法总结+详解_js中数组的方法-CSDN博客
6.常用字符串方法
7.深浅拷贝
内存中一共分为栈内存和堆内存两大区域,所谓深浅拷贝主要是对js引用类型数据进行拷贝一份,浅拷贝就是引用类型数据相互赋值之后,例obj1=obj2;如果后面的操作中修改obj1或者obj2,这个时候数据是会进行相应的变化的,因为在内存中引用类型数据是存储在堆内存中,堆内存中存放的是引用类型的值,同时会有一个指针地址指向栈内存,两个引用类型数据地址一样,如果其中一个发生变化另外一个都会有影响。而深拷贝则不会,深拷贝是会在堆内存中重新开辟一块空间进行存放。
简单来说就是B复制了A,如果A发生了改变,如果B随之变化,那么是浅拷贝,如果B并没有发生变化,则是深拷贝。
(1)浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性值是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址。
- 拷贝对象:Object.assign() / {...obj}
- 拷贝数组:arr.concat() / [...arr]
// Object.assign
let a = {id:1,name:'a',obj:{id:999}};
function fun(obj){
let o = {};
Object.assign(o,obj);
return o;
}
let a2 = fun(a);
a2.name ='a2';
a2.obj.id = 888;
console.log(a); // {id:1,name:'a',obj:{id:888}}
console.log(a2); // {id:1,name:'a2',obj:{id:888}}
// concat()
let list = ['a','b','c'];
let list2 = list.concat();
list2.push('d');
console.log(list); // ['a','b','c']
console.log(list2); // ['a','b','c','d']
// slice()
let list = ['a','b','c'];
let list2 = list.slice();
list2.push('d');
console.log(list); // ['a','b','c']
console.log(list2); // ['a','b','c','d']
// 二维数组
let list = ['a','b','c',['d','e','f']];
let list2 = list.concat();
list2[3][0] = 'a';
console.log(list); // ['a','b','c',['a','e','f']]
console.log(list2); // ['a','b','c',['d','e','f']]
(2)深拷贝:不管原数据中值是什么类型的数据,拷贝后的新数据跟原数据是相互独立,没有关联的。
- 通过JSON.stringify()实现:
let a = {
name : 'a',
age : 20,
obj : {id:999},
action : function(){
console.log(this.name)
}
}
let b = JSON.parse(JSON.stringify(a))
a.name = 'b'
a.obj.id = 888
console.log(a) // {name:'b',age:20,obj:{...},action:f}
console.log(b) // {name:'a',age:20,obj:{...}}
缺点:取不到值为undefined的键、NaN和无穷转变为null、无法获取自己原型链上的内容,只能获取Object原型内容、date对象转为date字符串。
- 通过递归实现:
let a = {
name: 'a',
skin: ['red', 'blue', 'yellow', ['123', '456']],
child: {
work: 'none',
obj: {
id: 111
}
},
action: function() {
console.log(this.name)
}
}
// 封装的递归方法
function deepClone(obj) {
let newObj = Array.isArray(obj) ? [] : {}
for (let i in obj) {
// 判断是不是对象
if(typeof obj[i] === 'object') {
// 递归解决多层拷贝
newObj[i] = deepClone(obj[i])
} else {
newObj[i] = obj[i]
}
}
return newObj
}
let b = deepClone(a)
b.child.obj.id = 222
b.skin[3][0] = 'pink'
console.log(a)
console.log(b)
JavaScript深拷贝看这篇就行了!(实现完美的ES6+版本)_javascript 深拷贝-CSDN博客
8.数据类型以及判断方法
(1)基本数据类型:基本类型值在内存中占据固定大小,直接存储在栈内存中。
- number数字型:正数、负数、小数等
- string字符串型:通过''、""、``号包裹的数据都叫字符串
- boolean布尔型:两个固定的值true和false
- undefined未定义型:只声明变量,不赋值
- null空类型:代表“无”、“空”、或“值未知”的特殊值
(2)引用数据类型(Object对象):Array、Function、Date、RegExp等。引用类型在栈中存储了指针,这个指针指向内存中的地址,真实的数据存放在堆内存里。假如两个引用类型同时指向了一个地址,修改其中一个,另一个也会改变。
(3)数据类型判断方法:
- typeof():用于判断基本数据类型
- instanceof():只能判断引用数据类型
- constructor:可以判断基本数据类型和引用数据类型
- object.prototype.toString.call():完美解决方案
判断数据类型的几种方式_判断数据类型的方式_染墨^O^的博客-CSDN博客
9.判断一个变量是否数组
(1)arr instanceof Array
(2)arr.constructor === Array
(3)Array.isArray(arr)
(4)Object.prototype.toString.call(arr) === '[object Array]'
(5)arr.__proto__ === Array.prototype
10.事件委托
又叫事件代理,原理就是利用了事件冒泡的机制来实现,也就是说把子元素的事件绑定到了父元素的身上。如果子元素阻止了事件冒泡,那么委托也就不成立。
addEventListener('click', function, true/false):默认是false(事件冒泡),true(事件捕获)
阻止事件冒泡:event.stopPropagation()
阻止默认行为:event.preventDefault()
好处:提高性能,减少事件的绑定,减少内存的占用。
11.事件循环
JS是一个单线程的脚本语言。主线程先执行同步任务,然后才去执行任务队列里的任务,如果在执行宏任务之前有微任务,那么要先执行微任务。全部执行完之后等待主线程的调用,调用完之后再去任务队列中查看是否有异步任务,这样一个循环往复的过程就是事件循环(Event Loop)。
宏任务:setTimeout、setInterval、Ajax、DOM事件
微任务:process.nexttick、promise、async/await
宏任务与微任务的区别_宏任务和微任务区别_刘 同 学的博客-CSDN博客
面试题:
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
async1()
new Promise(function(resolve) {
console.log('promise1')
resolve()
}).then(function() {
console.log('promise2')
})
console.log('script end')
输出结果:
12.垃圾回收机制
概念:JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。
内存泄漏:JS里已经分配内存地址的对象,但是由于长时间没有释放或没办法清除,造成长期占用内存的现象。这会让内存资源大幅浪费,最终导致运行速度慢,甚至崩溃的状况。
因素:一些为生命直接赋值的变量、一些未清空的定时器、过度的闭包、一些引用元素没有被清除等。
引用计数法:
- 跟踪记录被引用的次数。
- 如果被引用了一次,那么就记录次数1,多次引用会累加。
- 如果减少一个引用就减1。
- 如果引用次数是0,则释放内存。
注意:如果两个对象相互引用,尽管它们已不再使用,但垃圾回收器不会进行回收,导致内存泄漏。
标记清除法:
- 标记清除法将“不再使用的对象”定义为“无法达到的对象”
- 从根部(在JS中就是全局对象)出发定时扫描内存中的对象,凡是能从根部到达的对象,都是还需要使用的。
- 那些无法由根部出发到达的对象被标记为不再使用,稍后进行回收。
面试题——js垃圾回收机制和引起内存泄漏的操作_js垃圾回收机制面试题-CSDN博客
13.var、const和let的区别
(1)var是ES5提出的,let和const是ES6提出的。
(2)var声明的变量存在变量提升现象,let和const声明的变量不存在变量提升现象。
(3)var允许重复声明同一个变量,let和const在同一作用域下不允许重复声明同一个变量。
(4)var声明的变量不存在块级作用域(函数作用域),let和const声明的变量存在块级作用域,并且声明的变量只在所声明的块级作用域内有效。
(5)var不存在暂时性死区,let和const存在暂时性死区。(let和const会形成封闭的作用域,若在声明之前使用变量,就会报错)
(6)var声明的变量在window上,let和const声明的不在。
14.this的指向
this是一个指针型变量,它动态指向当前函数的运行环境。
(1)箭头函数中:this→声明时所在作用域下的this
(2)全局作用域中:this→window
(3)普通函数中:this→调用者
(4)事件绑定中:this→事件源
(5)定时器中:this→window
(6)构造函数中:this→实例化对象
15.new操作符
(1)在内存中创建一个新的空对象。
(2)把空对象和构造函数通过原型链进行链接。
(3)把构造函数的this绑定到新的空对象身上。
(4)执行构造函数里面的代码,给这个新对象添加属性和方法。
(5)根据构造函数返回的类型判断,如果是值类型,则返回对象。如果是引用类型,则返回这个引用类型。(构造函数里不需要return)
16.ES6新特性
(1)let、const关键字
(2)箭头函数
(3)展开运算符
(4)解构赋值
(5)模板语法
(6)rest参数
(7)简化对象写法
(8)函数参数默认值
(9)for of 和 for in
(10)symbol数据类型
(11)Iterator迭代器
(12)Gnerators生成器
(13)Promise
(14)Set
(15)Map
(16)class类
(17)模块化
ECMAScript 6 入门 - 《阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版》 - 书栈网 · BookStack
17.模块化
(1)CommonJS:module.exports={ }导出,require导入
- CommonJS可以动态加载语句,代码发生在运行时。
- CommonJS混合导出如果使用exports导出单个值后,就不能再导出一个对象值。
- CommonJS导出值是拷贝,可以修改导出的值,这在代码出错时,不好排查引起变量污染。
(2)ESM:export default导出,import导入
- ES Module是静态的,不能动态加载语句,只能声明在该文件的最顶部,代码发生在编译时。
- ES Module混合导出,单个导出,默认导出互不影响。
- ES Module导出和引用值之间都存在映射关系,并且值都是可读的,不能修改。
18.实现继承的方式
(1)原型链继承:让一个构造函数的原型是另一个类型的实例,那么这个构造函数new出来的实例就具有该实例的属性。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。(子类型的原型为父类型的一个实例对象)
function Parent() {
this.isShow = true
this.info = {
name: "mjy",
age: 18,
}
}
Parent.prototype.getInfo = function() {
console.log(this.info)
console.log(this.isShow)
}
function Child() {}
Child.prototype = new Parent()
let Child1 = new Child()
Child1.info.gender = "男"
Child1.getInfo() // {name: 'mjy', age: 18, gender: '男'} ture
let child2 = new Child()
child2.isShow = false
console.log(child2.info.gender) // 男
child2.getInfo() // {name: 'mjy', age: 18, gender: '男'} false
优点:写法方便简洁,容易理解。
缺点:对象实例共享所有继承的属性和方法。传教子类型实例的时候,不能传递参数,因为这个对象是一次性创建的(没办法定制化)。
(2)借用构造函数继承:在子类型构造函数的内部调用父类型构造函数;使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上。
function Person(name, age) {
this.name = name
this.age = age
}
function Student(name, age, price) {
// 利用call(),将Student的this传递给Person构造函数
// 相当于 this.Person(name, age)
Person.call(this, name, age)
// this.name = name
// this.age = age
this.price = price
}
let stu = new Student('Tom', 20, 14000)
console.log(stu.name, stu.age, stu.price)
优点:解决了原型链实现继承的不能传参的问题和父类的原型共享的问题。
缺点:借用构造函数的缺点是方法都在构造函数中定义,因此无法实现函数复用。在父类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
(3)组合继承:将原型链和借用构造函数组合到一块。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有自己的属性。
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype,setName = function(name) {
this.name = name
}
function Student(name, age, price) {
Person.call(this, name, age)
this.price = price
}
Student.prototype = new Person()
Student.prototype.constructor = Student
Student.prototype.setPrice = function(price) {
this.price = price
}
let stu = new Student('Tom', 20, 14000)
stu.setName('Bob')
stu.setPrice(16000)
console.log(stu.name, stu.age, stu.price)
优点:解决了原型链继承和借用构造函数继承造成的影响。
缺点:无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
19.ES5和ES6实现继承的区别
(1)ES5继承是通过原型或构造函数机制来实现。
(2)ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上。
(3)ES6的继承实质上是先创建父类的实例对象this(必须先调用父类的super方法),然后再用子类的构造函数修改this。
(4)ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor中调用super方法,否则会报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。
20.箭头函数与普通函数的区别
(1)箭头函数都是匿名函数。
(2)箭头函数不能用于构造函数,不能使用new。
(3)普通函数中this总是指向调用它的对象,如果用作构造函数,this指向创建的对象实例。箭头函数中this是静态的,指向永远是声明时所在作用域下的this值,它并没有自己的this。call、apply、bind也无法改变this的指向。
(4)箭头函数不具有prototype原型对象。
(5)箭头函数不绑定arguments,取而代之使用rest参数。
21.call、apply、bind的区别
(1)call函数
概念:call()方法调用一个对象。简单理解为调用函数的方式,但是它可以改变函数的this指向。
调用函数:fn.call('形参1','形参2',...)
改变指向:fn.call(this,'形参1','形参2',...)
应用场景:让函数立即执行,改变this的指向,实现继承。
(2)apply函数
概念:apply()方法调用一个函数。简单理解为调用函数的方式,但是它可以改变函数的this指向。传递参数的时候,程序会自动转换为相应的类型,数字转换为数字型,字符串转换为字符串类型。
调用函数:fn.apply([形参以伪数组形式])
改变指向:fn.apply(this,[形参伪数组形式])
(3)bind函数
概念:bind()方法不会调用函数,但是能改变函数内部this指向,返回的是原函数改变this之后产生的新函数。如果只是想改变this指向,并且不想调用这个函数的时候,可以使用bind。
改变指向:fn.bind(this,'形参1','形参2',...)
(4) 三者的区别
- call和apply会调用函数并且改变函数内部this指向
- call和apply传递的参数形式不一样, call传递aru1,aru2...形式,apply必须数组形式[arg]
- bind不会调用函数,可以改变函数内部this指向
- bind返回对应函数,便于稍后调用;apply、call则是立即调用
22.for in和for of的区别
forEach、for in 、 for of三者的区别-CSDN博客
23.对Promise的理解
Promise难懂?一篇文章让你轻松驾驭_promise很难理解-CSDN博客
24.async和await
Promise用then链来解决多层回调问题,但是连续调用then会使得代码很冗长,可读性不好,所以在ES7中提出了用async和await来解决这一问题。async会将其后的函数(函数表达式或Lambda)的返回值封装成一个Promise对象,而await会等待这个Promise完成,并将其结果返回出来。
理解异步函数async和await的用法_async await用法-CSDN博客
25.柯里化
概念:把一个多参数的函数转化为单参数函数的方法。
作用:惰性求值、动态生成函数。
26.判断元素是否出现在视口
(1)offsetTop - scrollTop <= 视口高度
(2)getBoundingClientRect()
(3)IntersectionObserver
前端必知:如何判断元素出现在视口内_判断元素进入视口_清颖~的博客-CSDN博客
27.浮点数精度问题
问题:在js中整数和浮点数都属于Number数据类型,所有数字都是以64位浮点数形式储存,即便整数也是如此。对于64位的浮点数在内存中的表示,最高的1位是符号位,接着的11位是指数,剩下的52位为有效数字。在测试js浮点数进行加减乘除计算时,可能会出现问题,例如0.1+0.2=0.30000000000000004。
原因:将0.1和0.2转换成二进制后,变成了一个无限循环的数字,但计算机不允许无限循环,对于无限循环的小数,计算机会进行舍入处理。双精度浮点数的小数部分最多支持52位,所以两者相加之后得到因浮点数小数位的限制而截断的二进制数字,这时候我们再把它转换为十进制,就成了0.30000000000000004。
0.1 → 0.0001 1001 1001 1001…(无限循环)
0.2 → 0.0011 0011 0011 0011…(无限循环)
解决:
(1)Math.js是专门为js和Node.js提供的一个广泛的数学库。它具有灵活的表达式解析器,支持符号计算,配有大量内置函数和常量,并提供集成解决方案来处理不同的数据类型。
(2)toFixed()方法使用定点表示法来格式化一个数,会对结果进行四舍五入。语法为numObj.toFixed(digits),参数digits表示小数点后数字的个数,介于0到20之间,忽略该参数则默认为0。该方法返回的是一个字符串。
28.怎样理解JS是单线程的
进程:一个在内存中运行的应用程序。进程是CPU资源分配的最小单位,每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,进程之间不会共享资源。
线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。线程是CPU调度的最小单位,一个进程至少有一个线程,多个线程之间共享进程的资源。
区别:
- 根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。
- 包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
- 内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的。
- 影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
- 执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行。