简介
1.js类型
首先,现在js的类型有以下几种:
基本类型
: String , Number , null , undefined , Boolean , Symbol , Bigint引用类型
: object , function
(1)基本类型和应用类型的区别
基本数据类型是按值访问的,因为可以直接操作保存在变量中的实际值。示例:
var a = 10;
var b = a;
b = 15;
console.log(a)//输出10,这说明b只是保存了a复制的一个副本。所以,b的改变,对a没有影响。
复制代码
如图:
javascript的引用数据类型是保存在堆内存中的对象。实例:
var obj1 = {};
var obj2 = obj1;
obj2.name = "小白";
console.log(obj1.name); // 小白
复制代码
如图:
(2)类型转换
1.可以转为false的5个值:NaN,'',0,null,undefined
2.Number([value])把其他数据类型转换我number数字类型
字符串转换为数字:空字符串是0,如果字符串中出现任意一个非有效数字字符,结果都是NaN 布尔转换为数字:true=>1 false=>0 null=>0 undefined=>NaN symbol不能转换为数字,报错 bigInt可以转换为数字 引用类型(对象或者函数) 首先获取它的[Symbol.toPrimitive]属性值 如果没有这个属性,其次获取它的valueOf 如果还是没有原始值,再转换为字符串toString,然后再转换为数字Number 3.parseInt/parseFloat([value])把其他数据类型转换为数字类型
需要保证[value]是一个字符串,如果不是则首先隐式的把其转换为字符串[value].toString() 从字符串左侧第一个字符开始向右查找,把找到的的有效数字字符,转换为数字(遇到一个非有效数字字符则停止查找,不论后面是否还有有效数字,都不再查找) 如果一个有效数字字符都没有找到,结果都是NaN
(3)类型检测
关于类型检测,可以看看我之前写的文章: js四种类型检测方法
(4)题目测试
首先这道题考验了parseInt的语法,即parseInt([string],[radix]可选),第一个参数是要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用 ToString 抽象操作)。字符串开头的空白符将会被忽略。第二个参数radix则表示进制。
let arr = [27.2,0,'0013','14px',123];
arr = arr.map(parseInt);
console.log(arr);
//这里的map函数中的结构为 item index 27.2 0 0 1 ‘0013’ 2 ‘14px’ 3 123 4
由于radix中只接受2-16进制,因此进制为1的直接转化为NaN, 进制运算举例: parseInt(‘14px’,3),首先取出14,然后计算3进制,由于1和4中,4不满足3进制,因此只有1,所以得出1*3^0 因此结果为[27, NaN, 1, 1, 27] 复制代码
2.关于闭包
关于闭包,根据红宝书第3版的解释是:闭包是指有权访问另一个函数作用域中的变量的函数。
总结:
• 函数执行会形成一个私有上下文,如果上下文中的某些内容(一般指的是堆内存地址)被上下文以外的些事物(例如:变量、事件绑定)所占用,则当前上下文不能被出栈释放(浏览器的垃圾回收机制GC所决定的) > => 闭包的机制:形成一个不被释放的上下文; • 保护:保护私有上下文中的私有变量和外界互不影响> • 保存:上下文不被释放,那么上下文中的私有变量和值都会保存起来,可以供其下级上下文使用 • 弊端:如果大量使用闭包,会导致栈内存太大,页面渲染变慢,性能受到影响,所以真是项目中想要合理应用闭包;某些代码会导致栈溢出或者内存泄露,这些操作都是需要我们注意的.
1.解决循环问题
用var
var li = document.querySelectorAll("li");
for(var i=0;i<li.length;i++){
li[i].οnclick= function(){
console.log(i);//显示结果都为3
}
}
复制代码
使用闭包
for(var i=0;i<li.length;i++){
(function(i){
li[i].οnclick= function(){
console.log(i) //输出0,1,2
}
})(i)
} 复制代码
使用let
for(let i=0;i<li.length;i++){
li[i].οnclick= function(){
console.log(i);//显示结果为0,1,2
}
}
复制代码
2.compose函数
当我们遇到const fn1 = x => x*2 , fn2 = x => x+3 , fn3 = x =>x-1 ,想要把处理数据的函数像管道一样连接起来, 然后让数据穿过管道得到最终的结果,如果不通过compose函数处理,则是fn3(fn2(fn1(x))),这样看起来非常不美观。因此我们可以构建一个compose函数,它接受任意多个函数作为参数(这些函数都只接受一个参数)
function compose(...args){
return function(x){
let len = args.length;
if(args == 0) return x;
if(len == 1) return args[0](x);
return args.reduceRight((pre,cur)=>{
return cur(pre)
},x)
}
}
const fn4 = compose(fn1,fn2,fn3);
console.log(fn4(5))
复制代码
redux中compose源码:
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
复制代码
3.原型链的理解
关于原型:
1.所有引用类型都有一个__proto__(隐式原型)属性,属性值是一个普通的对象 2.所有函数都有一个prototype(原型)属性,属性值是一个普通的对象 3.所有引用类型的__proto__属性指向它构造函数的prototype
如
![function Person(name){this.name = name}
let p1 = new Person("小白");
console.dir(p1)
console.log(p1.proto == Person.prototype)//true console.log(Person.prototype.constructor == Person)//true 复制代码
由此我们可以通过类型.prototype去看他们的API,可以非常清楚的学习到方法的使用,这个可以作为当你忘记api时及时查找的方法
关于原型链:当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链
看Person函数的原型链:
由图所知实例person会通过__proto__这个属性找到Person.prototype,再由Person.prototype.__proto__往上寻找,一直到null为止,从而形成了一条明显的长链.
4.手写函数
(1)new
new原理:
1.new关键字会首先创建一个空对象 2.将这个空对象的原型对象指向构造函数的原型属性,从而继承原型上的方法 3.将this指向这个空对象,执行构造函数中的代码,以获取私有属性 4.如果构造函数返回了一个对象res,就将该返回值res返回,如果返回值不是对象,就将创建的对象返回
ES5写法
function _new(target){
var obj = {},
params = [].splice.call(arguments,1),
result;
obj.proto = target.prototype; result = target.apply(obj,params);
if(result!=null && /(function|object)/.test(typeof result)){ return result; } return obj; } 复制代码
ES6写法
function _new(target,...params){
let obj = Object.create(target.prototype),
result = target.call(obj,...params);
if(result!=null && /^(function|object)$/.test(typeof result)){
return result;
}
return obj;
}
复制代码
(2)call
原理
给CONTEXT设置一个属性(属性名尽可能保持唯一,避免我们自己设置的属性修改默认对象中的结构,例如可以基于Symbol实现,也可以创建一个时间戳名字),属性值一定是我们要执行的函数(也就是THIS,CALL中的THIS就是我们要操作的这个函数) 接下来基于CONTEXT.XXX()成员访问执行方法,就可以把函数执行,并且改变里面的THIS(还可以把PARAMS中的信息传递给这个函数即可);都处理完了,别忘记把给CONTEXT设置的这个属性删除掉(人家之前没有你自己加,加完了我们需要把它删了)
Function.prototype.call = function(context,...params){
let key = Symbol('key'),//设置唯一值
result;
!/^(object|function)$/.test(typeof context) ? context = Object(context) :null;
context !=null ? null : context = window;//如果context为null或者undefined,直接赋值为window
context[key] = this; result = contextkey ;//返回值 delete context[key]; return result; } 复制代码
(3)apply
原理:
Function.prototype.apply = function(context,params = []){
let key = Symbol('key'),
result;
!/^(object|function)$/.test(typeof context) ? context = Object(context) :null;
context !=null ? null : context = window;
context[key] = this; result = contextkey ; delete context[key]; return result; } 复制代码
(4)bind
原理:
bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的call属性)。 当目标函数被调用时 this 值绑定到 bind() 的第一个参数,该参数不能被重写。绑定函数被调用时,bind() 也接受预设的参数提供给原函数。 一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
Function.prototype.bind = function(context,...params){
let self = this;
return funtion(...innerArgs){
params = params.concat(...innerArgs);
return self.call(context,...params);
}
}
复制代码
(5)防抖
思路:
在当前点击完成后,我们等wait这么长的时间,看是否还会触发第二次,如果没有触发第二次,属于非频繁操作,我们直接执行想要执行的函数func:如果触发了第二次,则以前的不算了,从当前这次再开始等待...
/*
防抖:
@params:
func[function]:最后要触发执行的函数
wait[number]:频繁设定的界限
immediate[boolean]:默认多次操作,我们识别的是最后一次,但是immediate=true,让其识别第一次
@return
可以被调用执行的函数
*/
function debounce(func,wait = 300,immediate = false){
let timer = null;
return function anonymous(...params){
let now = immediate && !timer;
//每次点击都把之前设置的定时器清除掉
clearInterval(timer)
//重新设置一个新的定时器监听wait事件内是否触发第二次
timer = setTimeout(() => {
timer = null;//垃圾回收机制
//wait这么久的等待中,没有触发第二次
!immediate ? func.call(this,...params) : null;
}, wait);
//如果是立即执行
now ? func.call(this,...params) : null;
}
} 复制代码
(6)节流
思路:
函数节流:在一段频繁操作中,可以触发多次,但是触发的频率由自己指定
/*
@params:
func[function]:最后要触发执行的函数
wait[number]:触发的频率
@return
可以被调用执行的函数
*/
function throttle(func,wait = 300){
let timer = null,
previous = 0;//记录上一次操作时间
return function anonymouse(...params){
let now = new Date(),//记录当前时间
remaining = wait - (now - previous);//记录还差多久达到我们一次触发的频率
if(remaining <= 0){
//两次操作的间隔时间已经超过wait了
window.clearInterval(timer);
timer = null;
previous = now;
func.call(this,...params);
}else if(!timer){
//两次操作的间隔时间还不符合触发的频率
timer = setTimeout(() => {
timer = null;
previous = new Date();
func.call(this,...params);
}, remaining);
}
}
}
复制代码
(7)浅拷贝
数组的浅拷贝:
let arr = [1,2,3]
let newArr = arr.slice();
复制代码
let arr = [1,2,3]
let newArr = arr.concat();
复制代码
对象浅拷贝
[注意]这里的toType请参考我写的jquery工具方法,链接:
jquery工具方法
function clone(obj){
var type = toType(obj);
if(/^(boolean|number|string|null|undefined|bigInt|symbol)$/.test(type)) return obj;
if(/^(regExp|date)$/.test(type)) return new obj.constructor(obj);
if(/^(function)$/.test(obj)) return function(){
obj()
};
if(/^error$/.test(type))return new obj.constructor(obj.message);
var target = {},
keys = this.getOwnProperty(obj);
Array.isArray(target) ? target = [] : null;
keys.forEach(item=>{
target[item] = obj[item]
})
return target
} 复制代码
(8)深拷贝
运用JSON.stringify+JSON.parse
[注意]这个方法有缺陷:
正则会被处理为空对象 具备函数/symbol/undefined属性值直接被干掉 BigInt还处理不了,会报错 日期对象最后还是字符串
let obj = {
a:1,
b:/^$/,
c:undefined,
d:new Date()
}
console.log(JSON.parse(JSON.stringify(obj)));
复制代码
如图:
手写深克隆
function deepClone(obj,cache = new Set()){//深克隆 cache处理self属性,防止死递归
//只有数组和对象我们再处理深克隆,其余的按照浅克隆
let type = toType(obj);
if(!/^(array|object)$/.test(type)) return this.clone(obj);
if(cache.has(obj)) return obj;
cache.add(obj);
let keys = this.getOwnProperty(obj),
clone = {};
type === "array" ? clone = [] : null;
keys.forEach(key=>{
clone[key] = this.deepClone(obj[key],cache)
})
return clone
} 复制代码
(9)instanceof
function _instanceof(L,R){
// 验证如果为基本数据类型,就直接返回false
const baseType = ['string', 'number','boolean','undefined','symbol']
if(baseType.includes(typeof(L))) { return false }
let RP = R.prototype; //取 R 的显示原型
L = L.__proto__; //取 L 的隐式原型
while(true){ // 无线循环的写法(也可以使 for(;;) )
if(L === null){ //找到最顶层
return false;
}
if(L === RP){ //严格相等
return true;
}
L = L.__proto__; //没找到继续向上一层原型链查找
}
} 复制代码
(10)数组扁平化
flat方法实现
let arr = [1,2,[3,4,[5,[6]]]]
console.log(arr.flat(Infinity))//flat参数为指定要提取嵌套数组的结构深度,默认值为 1
复制代码
reduce实现
function fn(arr){
return arr.reduce((prev,cur)=>{
return prev.concat(Array.isArray(cur)?fn(cur):cur)
},[])
}
复制代码
5.浅谈js事件
关于事件,我们认为事件是可以被Javascript侦测到的行为,通俗的讲就是当用户与Web页面进行某些交互时,解释器就会创建响应的event对象以描述事件信息。【注意:事件并不是你自己添加才有的,而是浏览器自带的】
(1).事件绑定
<button οnclick="console.log(1)">点我</button>
复制代码
缺点:违反了内容与行为相分离的原则,在html中写js代码,强耦合,不利于复用代码。不推荐
let btn = document.querySelector(".btn");
btn.onclick = function(){
console.log(1);
}
复制代码
缺点:解决了强耦合性,但只能给一个事件对象绑定一个事件类型。 同样的事件类型,绑定两次,以 后一次 为准。
-DOM2级事件
同样的事件类型,绑定多次,每个都会执行,可以设置时捕获还是冒泡。
let btn = document.querySelector(".btn");
btn.addEventListener("click",function(){
console.log(1);
},false)//默认值为false,可不写,false表示冒泡
复制代码
冒泡与捕获
事件冒泡
即事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点,事件捕获
思想是不太具体的DOM节点应该更早接收到事件,而最具体的节点应该最后接收到事件
事件冒泡
<div class="my">click me</div>
如果你点击了页面中的<div>元素,那么这个click事件会按照如下顺序传播:
1.<div>
2.<body>
3.<html>
4.document
复制代码
事件捕获
<div class="my">click me</div>
如果你点击了页面中的<div>元素,那么这个click事件会按照如下顺序传播:
1.document
2.<html>
3.<body>
4.<div>
复制代码
事件流
事件流包括三个阶段:事件捕获阶段,处于捕获阶段,事件冒泡阶段
以前面的html为例,如图所示: