JS核心知识梳理
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
JS核⼼知识梳理
前⾔
本⽂⽬标
从JS的运⾏,设计,数据,应⽤四个⾓度来梳理JS核⼼的知识点
主题⼤纲
1. JS运⾏
变量提升
执⾏上下⽂
作⽤域
let
作⽤域链
闭包
事件循环
2. JS设计
原型
原型链
this
call
apply
bind
new
继承
3. JS数据
数据类型
数据的存储(深浅拷贝)
数据类型判断(隐式转换,相等和全等,两个对象相等)
数据的操作(数组遍历,对象遍历)
数据的计算(计算误差)
4. JS应⽤
防抖,节流,柯⾥化
⼀. JS运⾏
⼤概分为四个阶段
1. 词法分析:将js代码中的字符串分割为有意义的代码块,称为词法单元
浏览器刚拿到⼀个JS⽂件或者⼀个script代码段的时候,它会认为⾥⾯是⼀个长长的字符串
这是⽆法理解的,所以要分割成有意义的代码块,⽐如: var a = 1
2. 语法分析:将词法单元流转换成⼀颗抽象语法树(AST),并对⽣成的AST树节点进⾏处理,
⽐如使⽤了ES6语法,⽤到了let,const,就要转换成var。
为什么需要抽象语法树呢?
抽象语法树是不依赖于具体的,不依赖于语⾔的细节,⽅便做很多的操作
另⼀⽅⾯说,现在有许多语⾔,C,C++,Java,Javascript等等,他们有不同的语⾔规范
但是转化成抽象语法树后就都是⼀致的了,⽅便编译器对其进⾏进⼀步的增删改查等操作,
3. 预解析阶段:
会确定作⽤域规则
变量和函数提升
4. 执⾏阶段:
创建执⾏上下⽂,⽣成执⾏上下⽂栈
执⾏可执⾏代码,依据事件循环
1.作⽤域
指定了函数和变量的作⽤范围
分为全局作⽤域和函数作⽤域,
JS不像C,JAVA语⾔⼀样,没有块级作⽤域,简单说就是花括号的范围
2.变量和函数提升
全局变量和函数声明会提升
函数声明⽅式有三种,
function foo() {}
var foo = function () {}
var foo = new Function()
可归为两类,直接创建和变量赋值
变量赋值函数和赋值普通变量的优先级按位置来,变量名相同前者被覆盖
函数直接创建优先级⾼于变量赋值,同名取前者,与位置⽆关,也就是说函数直接创建即使再变量声明后⾯,也是优先级最⾼3. 执⾏上下⽂
有不同的作⽤域,就有不同的执⾏环境,我们需要来管理这些上下⽂的变量
执⾏环境分为三种,执⾏上下⽂对应执⾏环境
全局执⾏环境
函数执⾏环境
eval执⾏环境(性能问题不提)
1. 全局执⾏上下⽂
先找变量声明,
再找函数声明
2. 函数执⾏上下⽂
先找函数形参,和变量声明
把实参赋值给形参
找函数声明
多个函数嵌套,就会有多个执⾏上下⽂,这需要执⾏上下⽂栈来维护,后进先出
执⾏上下⽂⾥包含着变量环境和词法环境
变量环境⾥就包含着当前环境⾥可使⽤的变量
当前环境没有⽤哪的, 这就说到了作⽤域链
4. 作⽤域链
引⽤JS⾼程的定义:作⽤域链来保证对执⾏环境有权访问的变量和函数的有序访问
变量的查找顺序不是按执⾏上下⽂栈的顺序,⽽是由词法作⽤域决定的
词法作⽤域也就是静态作⽤域,由函数声明的位置决定,和函数在哪调⽤⽆关,也就js这么特殊
5. 静态作⽤域和动态作⽤域
词法作⽤域是在写代码或者定义时确定的
⽽动态作⽤域是在运⾏时确定的(this也是!)
var a = 2;
function foo() {
console.log(a); // 静态2 动态3
}
function bar() {
var a = 3;
foo();
}
bar();
复制代码
闭包
由于作⽤域的限制,我们⽆法在函数作⽤域外部访问到函数内部定义的变量,⽽实际需求需要,这⾥就⽤到了闭包
引⽤JS权威指南定义:闭包是指有权访问另⼀个函数作⽤域中的变量的函数
1. 闭包作⽤
for循环遍历进⾏事件绑定输出i值时为for循环的长度+1
这结果显⽰不是我们想要的, 因为JS没有块级作⽤域,var定义的i值,没有销毁,存储与全局变量环境中
在事件具体执⾏的时候取的i值,就是全局变量中经过多次计算后的i值
for(var i = 0;i < 3;i++){
document.getElementById(`item${i+1}`).onclick = function() {
console.log(i);//3,3,3
}
}
复制代码
闭包特性:外部函数已经执⾏结束,内部函数引⽤外部函数的变量依然保存在内存中,变量的集合可称为闭包
在编译过程中,对于内部函数,JS引擎会做⼀次此法扫描,如果引⽤了外部函数的变量,堆空间创建换⼀个Closure的对象,⽤来存储闭包变量利⽤此特性给⽅法增加⼀层闭包存储当时的i值,将事件绑定在新增的匿名函数返回的函数上
for(var i = 0;i < 3;i++){
document.getElementById(`item${i+1}`).onclick = make(i);
}
function make(e) {
return function() {
console.log(e)//0,1,2
};
复制代码
闭包注意
我们在不经意间就写成了闭包,内部函数引⽤外部函数的变量依然保存在内存中,
该销毁的没有销毁,由于疏忽或错误造成程序未能释放已经不再使⽤的内存,就造成了内存泄漏
同时注意闭包不会造成内存泄漏,我们错误的使⽤闭包才是内存泄漏
事件循环
JS代码执⾏依据事件循环
JS是单线程,通过异步保证执⾏不被阻塞
1. 执⾏机制
简单说就是,⼀个执⾏栈,两个任务队列
发现宏任务就放⼊宏任务队列,发现微任务就放⼊微任务队列,
执⾏栈为空时,执⾏微任务队列所有微任务,再取宏任务队列⼀个宏任务执⾏
如此循环
2. 宏&微任务 macroTask: setTimeout, setInterval, I/O, UI rendering microTask: Promise.then
⼆. JS设计
1. 原型
1. JS的设计
有new操作符,构造函数,却没有类的概念,⽽是使⽤原型来模拟类来实现继承
2. JS设计⼼路历程
JS在设计之初,给的时间较短,并且定义为简单的⽹页脚本语⾔,不⽤太复杂,且想要模仿Java的理念,(这也是为什么JS叫JavaScript的原因)
因此就借鉴了Java的对象、构造函数、new操作符理念,⽽抛弃掉了了复杂的class(类)概念
3. 继承机制
需要有⼀种继承的机制,来把所有对象联系起来,就可以使⽤构造函数
但是构造函数⽣成实例对象的缺点就是⽆法共享属性和⽅法
4. prototype属性
为解决上⾯问题,就引⼊了prototype属性,就是我们常说的原型
为构造函数设置⼀个prototype属性,实例对象需要共享的⽅法,都放在此对象上,
整个核⼼设计完成后,后⾯的API也就顺理成章
原型
每⼀个js对象在创建的时候就会与之关联另⼀个对象
这个对象就是原型,每个对象都会从原型继承属性
proto
每个对象都有⼀个属性叫proto,该属性指向对象的原型
构造函数的prototype属性等于实例化对象的proto属性
此属性并不是ES5 中的规范属性,只是为了在浏览器中⽅便获取原型⽽做的⼀个语法糖,
我们可以使⽤Object.getPrototype()⽅法获取原型
constructor 原型没有指向实例,因为⼀个构造函数可以有多个对象实例但是原型指向构造函数是有的,每个原型都有⼀个constructor属性指向关联的构造函数
function Per() {} // 构造函数
const chi = new Per() // 实例对象
chi.__proto__ === Per.prototype // 获取对象的原型也是就构造函数的prototype属性
Per.prototype.constructor === Per // constructor属性获取当前原型关联的构造函数
复制代码
实例与原型
读取实例属性找不到时,就会查找与对象关联的原型的属性,⼀直向上查找,
这种实例与原型之间的链条关系,这就形成了原型链
function Foo() {}
= 'tom'
const foo = new Foo()
= 'Jerry'
console.log(); // Jerry
delete
console.log(); // tom
复制代码
2.原型链
⾸先亮出⼤家熟悉的⽹图
就是实例与构造函数,原型之间的链条关系
实例的 proto 指向原型
构造函数的 prototype 属性指向原型
原型的 constructor 属性指向构造函数
所有构造函数的 proto 指向 Function.prototype
Function.prototype proto 指向 Object.prototype
Object.prototype proto 指向 null
函数对象原型(Function.prototype)是负责造构造函数的机器,包含Object、String、Number、Boolean、Array,Function。
再由构造函数去制造具体的实例对象
function Foo() {}
// 1. 所有构造函数的 __proto__ 指向 Function.prototype
Foo.__proto__ // ƒ () { [native code] }
Function.__proto__ // ƒ () { [native code] }
Object.__proto__ // ƒ () { [native code] }
// 2. 所有构造函数原型和new Object创造出的实例 __proto__ 指向 Object.prototype
var o = new Object()
o.__proto__ // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ ...}
Function.prototype.__proto__ // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ ...}
// 3. Object.prototype 指向null
Object.prototype.__proto__ // null
复制代码
2. this
1. 作为对象⽅法调⽤时,指向该对象 obj.b(); // 指向obj`
2. 作为函数⽅法调⽤, var b = obj.b; b(); // 指向全局window(函数⽅法其实就是window对象的⽅法,所以与上同理,谁调⽤就指向谁)
3. 作为构造函数调⽤ var b = new Per(); // this指向当前实例对象
4. 作为call与apply调⽤ obj.b.apply(object, []); // this指向当前指定的值
谁调⽤就指向谁
const obj = {a: 1, f: function() {console.log(this, this.a)}}
obj.f(); // 1
const a = 2;
const win = obj.f;
win(); // 2
function Person() {
this.a = 3;
this.f = obj.f;
}
const per = new Person()
per.f() // 3
const app = { a: 4 }
obj.f.apply(app); // 4
复制代码
this 指向是动态作⽤域,谁调⽤指向谁,
3. call
1. 定义:使⽤⼀个指定的 this 值和若⼲个指定的参数值的前提下调⽤某个函数或⽅法
2. 举例:
var obj = {
value: 1,
}
function foo (name, old) {
return {
value:this.value,
name,
old
}
}
foo.call(obj, 'tom', 12); // {value: 1, name: "tom", old: 12}
复制代码
3. 要求:
call改变了this的指向,
执⾏了foo函数
⽀持传参,参数个数不固定
this参数可以传null,传null时指向window
函数可以有返回值
⾮函数调⽤判断处理
4. 实现及思路
Function.prototype.call1 = function (context, ...args) {
if(typeof this !== 'function') {throw new Error('Error')} // ⾮函数调⽤判断处理
context = context || window; // ⾮运算符判定传⼊参数 null则指向window
const key = Symbol(); // 利⽤symbol创建唯⼀的属性名,防⽌覆盖原有属性
context[key] = this; // 把函数作为对象的属性,函数的this就指向了对象
const result = context[key](...args) // 临时变量赋值为执⾏对象⽅法
delete context[key]; // 删除⽅法,防⽌污染对象
return result // return 临时变量
}
复制代码
5. 应⽤场景改变this指向,⼤部分是为了借⽤⽅法或属性
1. 判断数据类型,借⽤Object的toString⽅法 Object.prorotype.toString.call()
2. ⼦类继承⽗类属性,function Chi() {Par.call(this)}
4. apply
1. 定义:使⽤⼀个指定的 this 值和⼀个数组(数组包含若⼲个指定的参数值)的前提下调⽤某个函数或⽅法
2. 举例:
var obj = {
value: 1,
}
function foo (name, old) {
return {
value:this.value,
name,
old
}
}
foo.apply(obj, ['tom', 12], 24); // {value: 1, name: "tom", old: 12}
复制代码
3. 要求:
call改变了this的指向,
执⾏了foo函数
⽀持传参,第⼆个参数为数组,之后的参数⽆效,数组内参数个数不固定,
this参数可以传null,传null时指向window
函数可以有返回值
⾮函数调⽤判断处理
4. 实现及思路
Function.prototype.apply1 = function (context, args) { // 与call实现的唯⼀区别,此处不⽤解构
if(typeof this !== 'function') {throw new Error('Error')} // ⾮函数调⽤判断处理
context = context || window; // ⾮运算符判定传⼊参数 null则指向window
const key = Symbol(); // 利⽤symbol创建唯⼀的属性名,防⽌覆盖原有属性
context[key] = this; // 把函数作为对象的属性,函数的this就指向了对象
const result = context[key](...args) // 临时变量赋值为执⾏对象⽅法
delete context[key]; // 删除⽅法,防⽌污染对象
return result // return 临时变量
}
复制代码
5. 应⽤场景
同 call
5. bind
1. 定义
bind⽅法创建⼀个新函数,
新函数被调⽤时,第⼀个参数为运⾏时的this,
之后的参数会在传递实参前传⼊作为它的参数
2. 举例
const obj = {
value: 1
};
function foo(name, old) {
return {
value: this.value,
name,
old
}
}
const bindFoo = foo.bind(obj, 'tom');
bindFoo(12); // {value: 1, name: "tom", old: 12}
复制代码
3. 要求
返回⼀个函数,第⼀个参数作为执⾏时this
可以在bind时传⼀部分参,执⾏函数时再传⼀部分参
bind函数作为构造函数,this失效,但参数有效,
并且作为构造函数时,原型应指向绑定函数的原型,以便实例来继承原型中的值
⾮函数调⽤bind判断处理
4. 实现及思路
Function.prototype.bind1 = function (context, ...args) {
if(typeof this !== 'function') {throw new Error('Error')} // ⾮函数调⽤判断处理
const self = this; // 保存当前执⾏环境的this
const Foo = function() {} // 保存原函数原型
const res = function (...args2) { // 创建⼀个函数并返回
return self.call( // 函数内返回
this instanceof res ? this : context, // 作为构造函数时,this指向实例,:作为普通函数this正常指向传⼊的context ...args, ...args2) // 两次传⼊的参数都作为参数返回
}
Foo.prototype = this.prototype; // 利⽤空函数中转,保证在新函数原型改变时bind函数原型不被污染
res.prorotype = new Foo();
return res;
}
复制代码
6. new
看看new出的实例能做什么
可访问构造函数的属性
访问prototype的属性
构造函数如有返回值,返回对象,实例则只能访问返回的对象中的属性,this⽆效
返回基本类型值,正常处理,this有效
function Persion(name, age) {
= name;
this.age = age;
}
Person.prototype.sayName = function () {
console.log()
}
var per = new Person('tom', 10)
// 10 可访问构造函数的属性
per.sayName // tom 访问prototype的属性
复制代码
new实现
var person = factory(Foo)
function factory() { // new是关键字,⽆法覆盖,函数替代
var obj = {}; // 新建对象obj
var con = [].shift.call(arguments); // 取出构造函数
obj._proto_ = con.prototype; // obj原型指向构造函数原型
var res = con.apply(obj, arguments); // 构造函数this指向obj
return typeof(res) === 'object' ? ret : obj;
}
复制代码
7. 继承
1. 原型链继承和原型式继承
⽆法向⽗类传参数,只能共⽤属性
2. 借⽤构造函数和寄⽣继承
⽅法不在原型上,每次都要重新创建
3. 组合继承
虽然解决了以上俩个问题,但是调⽤了两次⽗亲,
实例和原型上会⽤相同属性
4. 寄⽣组合继承
⽬前最优⽅案
寄⽣组合继承实现
function P (name) { = name; } // ⽗类上绑定属性动态传参
P.prototype.sayName = function() { // ⽗类原型上绑定⽅法
console.log(111)
}
function F(name) { // ⼦类函数⾥,⽗类函数利⽤`call`函数this指向⼦类,传参并执⾏
P.call(this, name)
}
const p = Object.create(P.prototype) // Object.create 不会继承构造函数多余的属性和⽅法
p.constructor = F; // constructor属性丢失,重新指向
F.prototype = p; // ⼦类原型指向中转对象
const c = new F('tom'); // ⼦类实例化
// tom
c.sayName() // 111
复制代码
三. JS数据
1.数据类型
JS分为基本类型和引⽤类型
基本类型:Boolean Undefined String Number Null Symbol
引⽤类型:Object Funciton Array 等
2.数据存储(深浅拷贝)
js数据类型中分为基本类型,和引⽤类型
1. 基本类型保存在栈内存中
赋值时编译系统重新创建⼀块内存来存储新的变量所以基本类型变量赋值后就断绝了关系
2. 引⽤类型保存在堆内存中
赋值时只是对对象地址的拷贝没有开辟新的内存,堆内存地址的拷贝,两者指向了同⼀地址
修改其中⼀个,另外⼀个就会收到影响
两个对象指向了同⼀地址修改其中⼀个就会影响另⼀个
特殊的数组对象⽅法(深拷贝⼀层)
1. obj2 = Object.assign({}, obj1)
2. arr2 = [].concat(arr1)
3. arr2 = arr1.slice(0)
4. arr2 = Array.form(arr1)
5. arr2 = [...arr1];
以上⽅法都只能深拷贝⼀层
JSON.parse(JSON.stringify(obj))(多层)不拷贝⼀个对象,⽽是拷贝⼀个字符串,会开辟⼀个新的内存地址,切断了引⽤对象的指针联系缺点
1. 时间对象 => 字符串的形式
2. RegExp、Error => 只得到空对象
3. function,undefined => 丢失
4. NaN、Infinity和-Infinity => 序列化成null
5. 对象是由构造函数⽣成 => 会丢弃对象的 constructor
6. 存在循环引⽤的情况也⽆法实现深拷贝
⼿动实现深拷贝(多层)
function Judgetype(e) {
return Object.prototype.toString.call(e).slice(8, -1).toLowerCase();
}
function Loop(param) {
let target = null;
if(Judgetype(param) === 'array') {
target = [];
for(let key of param.keys()){
target[key] = Deep(param[key]);
}
} else {
target = {};
Object.keys(obj).forEach((val) => {
target[key] = Deep(param[key]);
})
}
return target;
}
function Deep(param) {
//基本数据类型
if(param === null || (typeof param !== 'object' && typeof param !== 'function')) {
return param;
}
//函数
if(typeof param === 'function') {
return new Function('return ' + param.toString())();
}
return Loop(param);
}
复制代码
3.数据类型判断(类型判断,相等和全等,隐式转换,两个对象相等)
1. 类型判断 typeof
⽆法区分 object, null 和 array
对于基本类型,除 null 以外,均可以返回正确的结果。
对于引⽤类型,除 function 以外,⼀律返回 object 类型
typeof(1) // number
typeof('tom') // string
typeof(undefined) // undefined
typeof(null) // object
typeof(true) // boolean
typeof(Symbol(1)) // symbol
typeof({a: 1}) // object
typeof(function () {}) // function
typeof([]) // object
复制代码
instanceof
判断⼀个实例是否属于某种类型
[] instanceof Array; // true
{} instanceof Object;// true
var a = function (){}
a instanceof Function // true
复制代码
Object.prototype.toString.call
⽬前最优⽅案
toString() 是 Object 的原型⽅法,调⽤该⽅法,默认返回当前对象的 [[Class]] 。
这是⼀个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调⽤ toString() 就能返回 [object Object] 。
⽽对于其他对象,则需要通过 call / apply 来调⽤才能返回正确的类型信息
这⾥就是call的应⽤场景,巧妙利⽤call借⽤Object的⽅法实现类型的判断
function T(e) {
return Object.prototype.toString.call(e).slice(8, -1).toLowerCase();
}
T(1) // number
T('a') // string
T(null) // null
T(undefined) // undefined
T(true) // boolean
T(Symbol(1)) // symbol
T({a: 1}) // object
T(function() {}) // function
T([]) // array
T(new Date()) // date
T(/at/) // RegExp
复制代码
2. 相等和全等
== ⾮严格⽐较允许类型转换
=== 严格⽐较不允许类型转换
引⽤数据类型栈中存放地址,堆中存放内容,即使内容相同,但地址不同,所以两者还是不等的
const a = {c: 1}
const b = {c: 1}
a ===
b // false
复制代码
== 可能会有类型转换,不仅在相等⽐较上,在做运算的时候也会产⽣类型转换,这就是我们说的隐式转换布尔⽐较,先转数字
true == 2 // false
||
1 == 2
// if(X)
var X = 10
if(X) // true
10 ==> true
// if(X == true)
if(X == true) // false
10 == true
||
10 == 1
复制代码
数字和字符串做⽐较,字符串转数字
0 == '' // true
||
0 == 0
1 == '1' // true
||
1 == 1
复制代码
对象类型和原始类型的相等⽐较
[2] == 2 // true
|| valueOf() // 调⽤valueOf() 取⾃⾝值
[2] == 2
|| toString() // 调⽤toString() 转字符串
"2" == 2
|| Number() // // 数字和字符串做⽐较,`字符串`转`数字`
2 == 2
复制代码
⼩结 js使⽤某些操作符会导致类型变换, 常见是+,==
1. 运算时
加法存在⼀个字符串都转字符串
乘 - 除 - 减法字符串都转数字
2. 相等⽐较时
布尔⽐较,先转数字
数字和字符串做⽐较,字符串转数字
对象类型⽐较先转原始类型
课外⼩题实现 a == 1 && a == 2 && a == 3
const a = {
i: 1,
toString: function () {
return a.i++;
4. 判断两个对象值相等
引⽤数据类型栈中存放地址,堆中存放内容,即使内容相同,但地址不同,所以===判断时两者还是不等的但引⽤数据内容相同时我们如何判断他们相等呢?
// 判断两者⾮对象返回
// 判断长度是否⼀致
// 判断key值是否相同
// 判断相应的key值⾥的对应的值是否相同
这⾥仅仅考虑对象的值为object,array,number,undefined,null,string,boolean
关于⼀些特殊类型 `function date RegExp` 暂不考虑
function Judgetype(e) {
return Object.prototype.toString.call(e).slice(8, -1).toLowerCase();
}
function Diff(s1, s2) {
const j1 = Judgetype(s1);
const j2 = Judgetype(s2);
if(j1 !== j2){
return false;
}
if(j1 === 'object') {
if(Object.keys(s1).length !== Object.keys(s2).length){
return false;
}
s1[Symbol.iterator] = function* (){
let keys = Object.keys( this )
for(let i = 0, l = keys.length; i < l; i++){
yield {
key: keys[i],
value: this[keys[i]]
};
}
}
for(let {key, value} of s1){
if(!Diff(s1[key], s2[key])) {
return false
}
}
return true
} else if(j1 === 'array') {
if(s1.length !== s2.length) {
return false
}
for(let key of s1.keys()){
if(!Diff(s1[key], s2[key])) {
return false
}
}
return true
} else return s1 === s2
}
Diff( {a: 1, b: 2}, {a: 1, b: 3}) // false
Diff( {a: 1, b: [1,2]}, {a: 1, b: [1,3]}) // false
复制代码
其实对象遍历 return 也可以⽤for in,关于遍历原型的副作⽤可以⽤hasOwnproperty判断去弥补
对象for of 遍历还得⾃⼰加迭代器,⽐较⿇烦
return false
}
}
}
复制代码
4.数据操作(数组遍历,对象遍历)
1.数组遍历
`最普通 for循环` // 较为⿇烦
for(let i = 0,len = arr.length; i < len; i++) {
console.log(i, arr[i]);
}
`forEach` ⽆法 break return
`for in` 不适合遍历数组,
`for...in` 语句在w3c定义⽤于遍历数组或者对象的属性
1. index索引为字符串型数字,不能直接进⾏⼏何运算
2. 遍历顺序有可能不是按照实际数组的内部顺序
3. 使⽤for in会遍历数组所有的可枚举属性,包括原型
`for of` ⽆法获取下标
// for of 兼容1
for(let [index,elem] of new Map( arr.map( ( item, i ) => [ i, item ] ) )){
console.log(index);
console.log(elem);
}
// for of 兼容2
let arr = [1,2,3,4,5];
for(let key of arr.keys()){
console.log(key, arr[key]);
}
for(let val of arr.values()){
console.log(val);
}
for(let [key, val] of arr.entries()){
console.log(key, val);
}
2.
复制代码
2.对象遍历
1. for in
缺点:会遍历出对象的所有可枚举的属性, ⽐如prototype上的
var obj = {a:1, b: 2}
obj.__proto__.c = 3;
Object.prototype.d = 4
for(let val in obj) {
console.log(val) // a,b,c,d
}
// 优化
for(let val in obj) {
if(obj.hasOwnProperty(val)) { // 判断属性是存在于当前对象实例本⾝,⽽⾮原型上
复制代码
2. object.keys
var obj = { a:1, b: 2 }
Object.keys(obj).forEach((val) => {
console.log(val, obj[val]);
// a 1
// b 2
})
复制代码
3. for of
只有提供了 Iterator 接⼝的数据类型才可以使⽤ for-of
Array 等类型是默认提供了的
我们可以给对象加⼀个 Symbol.iterator 属性
var obj = { a:1, b: 2 }
obj[Symbol.iterator] = function* (){
let keys = Object.keys( this )
for(let i = 0, l = keys.length; i < l; i++){
yield {
key: keys[i],
value: this[keys[i]]
};
}
}
for(let {key, value} of obj){
console.log( key, value );
// a 1
// b 2
}
复制代码
5.数据计算(计算误差)
1. 0.1 + 0.2 = 0.30000000000000004
所有的数都会转换成⼆进制,逐位去计算,
⼩数⼆进制不能⼆等分的会⽆限循环
js数据存储 64 位双精度浮点数,这⾥不做赘述,超出会被截取(⼤数计算误差与限制也是因为这个)相加后再转换回来就会出现误差
2. 那么如何做出精确的计算呢
对于数字⼗进制本⾝不超过js存储位数的⼩数,可以同时变为整数,计算后再化为⼩数
function getLen(n) {
const str = n + '';
const s1 = str.indexOf('.')
if(s1) {
return str.length - s1 - 1
} else {
return 0
}
}
function add(n1, n2) {
const s1 = getLen(n1)
const s2 = getLen(n2)
const max = Math.max(s1, s2)
return (n1 * Math.pow(10, max) + n2 * Math.pow(10, max)) / Math.pow(10, max)
}
add(11.2, 2.11) // 13.31
对于超出存储位数的可以,转换成数组,倒序逐位相加,⼤于10进位,字符串拼接得到值function add(a, b) {
let i = a.length - 1;
let j = b.length - 1;
let carry = 0;
let ret = '';
while(i>=0|| j>=0) {
let x = 0;
let y = 0;
let sum;
if(i >= 0) {
x = a[i] - '0';
i--
}
if(j >=0) {
y = b[j] - '0';
j--;
}
sum = x + y + carry;
if(sum >= 10) {
carry = 1;
sum -= 10;
} else {
carry = 0
}
ret = sum + ret;
}
if(carry) {
ret = carry + ret;
}
return ret;
}
add('999999999999999999999999999999999999999999999999999999999999999', '1')
// 1000000000000000000000000000000000000000000000000000000000000000
复制代码
四. JS应⽤
1.防抖
场景:
搜索框输⼊下拉联想,请求后台接⼝,为了避免频繁请求,给服务器造成压⼒定义:
在事件触发n秒后执⾏,在⼀个事件触发n秒内⼜触发了该事件,就以新的事件为准
实现思想:
1. 定时器的执⾏与清除
2. apply 改变this指向
3. apply传参继承
function debounce(func, wait) {
var timeout;
return function () {
var context = this;
var args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
2.节流
场景:
可以将⼀些事件降低触发频率。
⽐如懒加载时要监听计算滚动条的位置,但不必每次滑动都触发,可以降低计算的频率,⽽不必去浪费资源;定义:
持续触发事件,规定时间内,只执⾏⼀次
1. ⽅法⼀:时间戳
实现思想:
触发时间取当前时间戳now, 减去flag时间戳(初始值为0)
如果⼤于规定时间,则执⾏,且flag更新为当前时间,
如果⼩于规定时间,则不执⾏
function foo(func, wait) {
var context, args;
var flag = 0;
return function () {
var now = +new Date();
context = this;
args = arguments;
if(now - flag > 0) {
func.apply(context, args);
flag = now;
}
}
}
复制代码
2. ⽅法⼆:定时器
实现思想:
判断当前是否有定时器,
没有就定义定时器,到规定时间执⾏,且清空定时器
有则不执⾏
function foo(func, wait) {
var context, args;
var timeout;
return function() {
if(!timeout){
setTimeout(()=>{
timeout = null;
func.apply(context, args);
}, wait)
}
}
}
复制代码
3.柯⾥化
1. 定义:将能够接收多个参数的函数转化为接收单⼀参数的函数,并且返回接收余下参数且返回结果的新函数
2. 特点:参数复⽤,提前返回,延迟执⾏
3. 实现过程
创建⼀个函数,利⽤ apply,给柯⾥化函数重新传⼊合并后的参数
利⽤reduce迭代数组所有项,构建⼀个最终返回值
function add(...args) {
}
fn.toString = function() {
return args.reduce(function(a, b) { return a + b;
})
}
return fn;
}
add(1)(2)(3).toString(); // 6。