大家好我是没钱的君子下流坯,用自己的话解释自己的知识
前端行业下坡路,甚至可说前端已死,我还想在前段行业在干下去,所以从新开始储备自己的知识。
从CSS——>Javascript——>VUE2——>Vuex、VueRouter、webpack——>VUE3——>pinia、Vite把前端基本的从新顺一遍,找出自己的不足。再去把一些组件给仔细研究一些自己以前没有发现的细节使用方法。
希望大家都能找到自己的出路。
1.数组去重
数字或字符串去重,效率高
function unique(arr) { var result = {}; // 利用对象属性名的唯一性来保证不重复 for (var i = 0; i < arr.length; i++) { if (!result[arr[i]]) { result[arr[i]] = true; } } return Object.keys(result); // 获取对象所有属性名的数组 }
任意数组去重,适配范围广,效率低
function unique(arr){ let result=[]; for(let i=0;i<arr.length;i++){ if(!result.includes(arr[i])){ result.push(arr[i]) } } return result; }
利用es6去重,使用范围广效率一般
function unique(arr){ return [new ...Set(arr)] }
2.下面代码执行结果
var a = 2;
var b = 5;
console.log(a === 2 || 1 && b === 3 || 4);
结果是true,因为 || 符号 只要有一个是正确的则直接返回结果不会去管后面的代码
考察的是逻辑运算符。在 || 里面,只要有一个为真,后面的直接短路,都不用去计算。所以 a === 2 得到 true 之后直接短路了,返回 true。
3.箭头函数有哪些特点
- 更简洁的语法,例如
- 只有一个形参就不需要用括号括起来
- 如果函数体只有一行,就不需要放到一个块中
- 如果 return 语句是函数体内唯一的语句,就不需要 return 关键字
- 箭头函数没有自己的 this,arguments,super
- 箭头函数 this 只会从自己的作用域链的上一层继承 this。
4.js中类的继承
JavaScript中使用的是组合式继承也被成为伪经典继承,组合继承综合了原型链继承和盗用构造函数继承两者的有点。缺点是效率问题,最主要的就是父类的构造函数始终会被调用两次,一次是创建子类原型时调用,一次是在子类构造函数中调用。下面用代码举例。
var person =function(name.age){ this.name=name; this.age=age; } person.prototype.test='this 这是测试test'; person.prototype.testFunc=function(){ console.log('这是 testFunc') } var Student=function(name,age,gender,score){ person.apply(this,[name,age]); this.gender=gender; this.score=score; } Student.prototype=new person(); Student.prototype.testStuFunc=function(){ console.log('这里是testStuFunc') } //上面的代码就是生成了一个person的父类构造函数,用Student子类函数的原型上添加父类的构造方法,这时候子类Student就可以使用父类上面的所有方法 var zhangsan = new Student("张三", 18, "男", 100); console.log(zhangsan.name); // 张三 console.log(zhangsan.age); // 18 console.log(zhangsan.gender); // 男 console.log(zhangsan.score); // 100 console.log(zhangsan.test); zhangsan.testFunc(); zhangsan.testStuFunc();
但是在组合继承中存在效率问题,比如在上面的代码中,我们其实调用了两次 Person,产生了两组 name 和 age 属性,一组在原型上,一组在实例上。
也就是说,我们在执行 Student.prototype = new Person( ) 的时候,我们是想要 Person 原型上面的方法,属性是不需要的,因为属性之后可以通过 Person.apply(this, [name, age]) 拿到,但是当你 new Person( ) 的时候,会实例化一个 Person 对象出来,这个对象上面,属性和方法都有。
圣杯继承的继承解决了这一问题,其基本思路就是不通过调用父类构造函数来给子类原型赋值,而是取得父类原型的一个副本,然后将返回的新对象赋值给子类原型
// target 是子类,origin 是基类 // target ---> Student, origin ---> Person function inherit(target, origin) { function F() { }; // 没有任何多余的属性 // origin.prototype === Person.prototype, origin.prototype.constructor === Person 构造函数 F.prototype = origin.prototype; // 假设 new F() 出来的对象叫小 f // 那么这个 f 的原型对象 === F.prototype === Person.prototype // 那么 f.constructor === Person.prototype.constructor === Person 的构造函数 target.prototype = new F(); // 而 f 这个对象又是 target 对象的原型对象 // 这意味着 target.prototype.constructor === f.constructor // 所以 target 的 constructor 会指向 Person 构造函数 // 我们要让子类的 constructor 重新指向自己 // 若不修改则会发现 constructor 指向的是父类的构造函数 target.prototype.constructor = target; } // 基类 var Person = function (name, age) { this.name = name; this.age = age; } Person.prototype.test = "this is a test"; Person.prototype.testFunc = function () { console.log('this is a testFunc'); } // 子类 var Student = function (name, age, gender, score) { Person.apply(this, [name, age]); this.gender = gender; this.score = score; } inherit(Student, Person); // 使用圣杯模式实现继承 // 在子类上面添加方法 Student.prototype.testStuFunc = function () { console.log('this is a testStuFunc'); } // 测试 var zhangsan = new Student("张三", 18, "男", 100); console.log(zhangsan.name); // 张三 console.log(zhangsan.age); // 18 console.log(zhangsan.gender); // 男 console.log(zhangsan.score); // 100 console.log(zhangsan.test); // this is a test zhangsan.testFunc(); // this is a testFunc zhangsan.testStuFunc(); // this is a testStuFunc
5.new 操作符都做了哪些事?
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
new 关键字会进行如下的操作:
步骤 1:创建一个空的简单 JavaScript 对象,即 { } ;
步骤 2:链接该对象到另一个对象(即设置该对象的原型对象);
步骤 3:将步骤 1 新创建的对象作为 this 的上下文;
步骤 4:如果该函数没有返回对象,则返回 this。
6.call、apply、bind 的区别 ?
call 和 apply 的功能相同,区别在于传参的方式不一样:
- fn.call(obj, arg1, arg2, …) 调用一个函数, 具有一个指定的 this 值和分别地提供的参数(参数的列表)。
- fn.apply(obj, [argsArray]) 调用一个函数,具有一个指定的 this 值,以及作为一个数组(或类数组对象)提供的参数。
bind 和 call/apply 有一个很重要的区别,一个函数被 call/apply 的时候,会直接调用,但是 bind 会创建一个新函数。当这个新函数被调用时,bind( ) 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
7.this 的指向哪几种 ?
总结起来,this 的指向规律有如下几条:
- 在函数体中,非显式或隐式地简单调用函数时,在严格模式下,函数内的 this 会被绑定到 undefined 上,在非严格模式下则会被绑定到全局对象 window/global 上。
- 一般使用 new 方法调用构造函数时,构造函数内的 this 会被绑定到新创建的对象上。
- 一般通过 call/apply/bind 方法显式调用函数时,函数体内的 this 会被绑定到指定参数的对象上。
- 一般通过上下文对象调用函数时,函数体内的 this 会被绑定到该对象上。
- 在箭头函数中,this 的指向是由外层(函数或全局)作用域来决定的。
个人感觉this的指向就是谁调用就指向谁,箭头函数内就是去找箭头函数的调用者或者父级。
8.什么是 js 的闭包?有什么作用?
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
闭包的用处:
- 匿名自执行函数
- 结果缓存
- 封装
- 实现类和继承
闭包就是内层函数有权访问外层函数的的作用域,反过来说外层函数作用域被内层函数使用无法释放。
9.事件委托以及冒泡原理
事件委托,又被称之为事件代理。在 JavaScript 中,添加到页面上的事件处理程序数量将直接关系到页面整体的运行性能。导致这一问题的原因是多方面的。
首先,每个函数都是对象,都会占用内存。内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM 访问次数,会延迟整个页面的交互就绪时间。
对事件处理程序过多问题的解决方案就是事件委托。
事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click 事件会一直冒泡到 document 层次。也就是说,我们可以为整个页面指定一个 onclick 事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。
事件冒泡(event bubbling),是指事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。
10.JS 的基本数据类型有哪些?基本数据类型和引用数据类型的区别
在 JavaScript 中,数据类型整体上来讲可以分为两大类:基本类型和引用数据类型
基本数据类型,一共有 8 种:
string,symbol,number,boolean,undefined,null,bigInt
其中 symbol、bigInt 类型是在 ES6 及后续版本里面新添加的基本数据类型。
引用数据类型,就只有 1 种:
object
基本数据类型的值又被称之为原始值或简单值,而引用数据类型的值又被称之为复杂值或引用值。
两者的区别在于:
原始值是表示 JavaScript 中可用的数据或信息的最底层形式或最简单形式。简单类型的值被称为原始值,是因为它们是不可细化的。
也就是说,数字是数字,字符是字符,布尔值是 true 或 false,null 和 undefined 就是 null 和 undefined。这些值本身很简单,不能够再进行拆分。由于原始值的数据大小是固定的,所以原始值的数据是存储于内存中的栈区里面的。
在 JavaScript 中,对象就是一个引用值。因为对象可以向下拆分,拆分成多个简单值或者复杂值。引用值在内存中的大小是未知的,因为引用值可以包含任何值,而不是一个特定的已知值,所以引用值的数据都是存储于堆区里面。
最后总结一下两者的区别:
访问方式
- 原始值:访问到的是值
- 引用值:访问到的是引用地址
比较方式
- 原始值:比较的是值
- 引用值:比较的是地址
动态属性
- 原始值:无法添加动态属性
- 引用值:可以添加动态属性
变量赋值
- 原始值:赋值的是值
- 引用值:赋值的是地址
11.防抖与节流
防抖就是规定时间n秒内点击多次只会触发最后一次,相当于n秒内无论点击多次只会触发一次。
function debounce(func, wait) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
}
//这是GPT写得 下面是我写的
//fn是需要防抖的函数,delay是等待时间
function debounce(fn, time = 500) {
let timer = null;
// 这里返回的函数是每次用户实际调用的防抖函数
return function(...args) { //...args是es6的剩余参数语法,将多余的参数放入数组,用来代替arguments对象
// 如果已经设定过定时器了就清空上一次的定时器
if(timer) {
//clearTimeout(timer);
timer=null
}
// 开始一个新的定时器,延迟执行用户传入的方法;注:定时器的返回值是一个数值,作为定时器的编号,可以传入clearTimeout来取消定时器
timer = setTimeout(() => { //这里必须是箭头函数,不然this指向window,要让this就指向fn的调用者
fn.apply(this, args);
}, time)
}
}
节流就是规定的n秒内点击事件只触发一次,也就是相当于计时器每n秒执行一次。
function throttle(func, wait) {
let lastCall = 0;
return function(...args) {
const context = this;
const now = Date.now();
if (now - lastCall >= wait) {
lastCall = now;
func.apply(context, args);
}
};
}
//这是GPT写得 下面是我写的
function throttle(fn,time){
let timer=null;
return function(){
if(!timer){//判断timer存在否
timer =setTimeout(function(){
fn();//这里就是需要执行的功能代码
//清除定时器,这里不能使用clearTimeout需要让定时器为空
timer=null
//原因是在setTimeout中是无法删除定时器的,因为其还在运作
},time)
}
}
}
12.操作DOM节点的添加、移除、复制、创建、和查找节点
1)创建新节点
createDocumentFragment( ) // 创建一个DOM 片段
createElement( ) // 创建一个具体的元素
createTextNode( ) // 创建一个文本节点
(2)添加、移除、替换、插入
appendChild( )
removeChild( )
replaceChild( )
insertBefore( ) // 在已有的子节点前插入一个新的子节点
(3)查找
getElementsByTagName( ) //通过标签名称
getElementsByName( ) // 通过元素的 Name 属性的值
getElementById( ) // 通过元素 Id,唯一性
querySelector( ) // 用于接收一个 CSS 选择符,返回与该模式匹配的第一个元素
querySelectorAll( ) // 用于选择匹配到的所有元素
13. 为什么 *console.log(0.2+0.1==0.3)*结果为false
因为浮点数的计算存在 round-off 问题,也就是浮点数不能够进行精确的计算。并且:
- 不仅 JavaScript,所有遵循 IEEE 754 规范的语言都是如此;
- 在 JavaScript 中,所有的 Number 都是以 64-bit 的双精度浮点数存储的;
- 双精度的浮点数在这 64 位上划分为 3 段,而这 3 段也就确定了一个浮点数的值,64bit 的划分是“1-11-52”的模式,具体来说:
- 就是 1 位最高位(最左边那一位)表示符号位,0 表示正,1 表示负;
- 11 位表示指数部分;
- 52 位表示尾数部分,也就是有效域部分
14.说一下 JS 中类型转换的规则
1. 隐性转换
当不同数据类型之间进行相互运算,或者当对非布尔类型的数据求布尔值的时候,会发生隐性转换。
预期为数字的时候:算术运算的时候,我们的结果和运算的数都是数字,数据会转换为数字来进行计算。
类型 转换前 转换后 number 4 4 string “1” 1 string “abc” NaN string “” 0 boolean true 1 boolean false 0 undefined undefined NaN null null 0 预期为字符串的时候:如果有一个操作数为字符串时,使用
+
符号做相加运算时,会自动转换为字符串。预期为布尔的时候:前面在介绍布尔类型时所提到的 9 个值会转为 false,其余转为 true
2. 显性转换
所谓显性转换,就是只程序员强制将一种类型转换为另外一种类型。显性转换往往会使用到一些转换方法。常见的转换方法如下:
转换为数值类型:
Number()
,parseInt()
,parseFloat()
转换为布尔类型:
Boolean()
转换为字符串类型:
toString()
,String()
当然,除了使用上面的转换方法,我们也可以通过一些快捷方式来进行数据类型的显性转换,如下:
转换字符串:直接和一个空字符串拼接,例如:
a = "" + 数据
转换布尔:!!数据类型,例如:
!!"Hello"
转换数值:数据*1 或 /1,例如:
"Hello * 1"
15.深拷贝和浅拷贝
浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制)
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。
浅拷贝方法
- 直接赋值
- Object.assign 方法:可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。当拷贝的 object 只有一层的时候,是深拷贝,但是当拷贝的对象属性值又是一个引用时,换句话说有多层时,就是一个浅拷贝。
- ES6 扩展运算符,当 object 只有一层的时候,也是深拷贝。有多层时是浅拷贝。
- Array.prototype.concat 方法
- Array.prototype.slice 方法
- jQuery 中的 . e x t e n d ∗ :在 ∗ j Q u e r y ∗ 中, ∗ .extend*:在 *jQuery* 中,* .extend∗:在∗jQuery∗中,∗.extend(deep,target,object1,objectN) 方法可以进行深浅拷贝。deep 如过设为 true 为深拷贝,默认是 false 浅拷贝。
深拷贝方法
- $.extend(deep,target,object1,objectN),将 deep 设置为 true
- JSON.parse(JSON.stringify):用 JSON.stringify 将对象转成 JSON 字符串,再用 JSON.parse 方法把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。这种方法虽然可以实现数组或对象深拷贝,但不能处理函数。
- 手写递归
示例代码如下:
function deepCopy(oldObj, newobj) { for (var key in oldObj) { var item = oldObj[key]; // 判断是否是对象 if (item instanceof Object) { if (item instanceof Function) { newobj[key] = oldObj[key]; } else { newobj[key] = {}; //定义一个空的对象来接收拷贝的内容 deepCopy(item, newobj[key]); //递归调用 } // 判断是否是数组 } else if (item instanceof Array) { newobj[key] = []; //定义一个空的数组来接收拷贝的内容 deepCopy(item, newobj[key]); //递归调用 } else { newobj[key] = oldObj[key]; } } }