1. JS创建变量的5种方式?
- var
- let
- const
- function
- export/import
2. var,let,const的区别?
- var声明的变量会挂载在window上,而let和const声明的变量不会;
- var声明变量存在变量提升,let和const不存在变量提升;
- let和const声明形成块作用域;
- 同一作用域下let和const不能声明同名变量,而var可以;
- const一旦声明必须赋值,不能使用null占位;声明后不能再修改 ;如果声明的是复合类型数据,可以修改其属性;
3. 数据类型有哪些?
- 基本数据类型:number,string,boolean,null,undefined,symbol,bigint;
- 复杂数据类型(引用数据类型):object,function;
4. 如何判断基本和复杂类型?
- typeof: 返回结果是一个字符串;
typeof null => "object";
typeof {a: 1} === "object";
typeof [1, 2, 4] === "object";
typeof new Date() === "object";- instanceof:只能检测引用类型,返回true/false,例如: arr intanceof Array;
- constructor:通过原型链追溯的数据类型,既可以判断基本数据类型又可以判断引用数据类型,返回true/false;
console.log(arr.constructor === Array); //true
console.log(date.constructor === Date); //true
console.log(fn.constructor === Function); //true- Object.prototype.toString.call():由于typeof无法区分对象、数组、函数的类型;
可以通过Object.prototype.toString.call()方法,判断某个对象属于哪种内置类型。
分为null、string、boolean、number、undefined、array、function、object、Date、math;
返回值为 [object 类型]
⭐可借鉴来自:https://www.cnblogs.com/onepixel/p/5126046.html
5. 数据类型转换有哪些方式?
分为显式转换和隐式转化:
①显式转换
转换为数值类型:Number(mix)、parseInt(string,radix)、parseFloat(string)
转换为字符串类型:toString(radix)、String(mix)
转换为布尔类型:Boolean(mix)
②隐式转化:
用于检测是否为非数值的函数:isNaN(mix)
递增递减操作符(包括前置和后置)、一元正负符号操作符
加法运算操作符
乘除、减号运算符、取模运算符
逻辑操作符(!、&&、||)
关系操作符(<, >, <=, >=)
相等操作符(==)
⭐可借鉴来自:https://www.cnblogs.com/Juphy/p/7085197.html
- NaN:not a number:不是一个数,但是属于number类型;
NaN == NaN //false NaN和任何其它值都不相等- isNaN():检测当前这个值是否是有效数字,如果不是有效数字,检测的结果是true;反之为false;
isNaN(0)//false isNaN(‘12’) //false isNaN(true) //false
isNaN(NaN)//true isNaN([]) //false- Number():
1️⃣把其它数据类型值转化为number类型的值,在使用Number转换的时候只要字符串中出现一个非有效数字字符,最后结果都是NaN;
Number('12') //12
Number('12px') //NaN
2️⃣把引用数据类型转换为number,首先把引用数据类型转换为字符串(toString),在把字符串转换为number即可 例如:[]->’’ ‘’->0
Numder([12,23]) => Numder('12,23' ) => NaN
Number({}) //NaN- parseInt():把其它数据类型转换为number;
⭐提取规则:从左到右依次查找有效数字字符,直到遇见非有效数字字符为止(不管后面是否还,有都不找了),把找到的转换为数字;
parseInt('12px') //12- parseFloat():parseInt的基础上可以识别小数点;
parseInt('12.53px') //12
parseFlot('12.53px') //12.53- toString():除undefined和null之外的所有类型的值都具有toString()方法,其作用是返回对象的字符串表示;
6. 基本数据类型和复杂数据类型的特点?
- 基本数据类型
变量名在栈上存储的是具体的数值
使用“==”是判断值是否相等- 复杂数据类型
变量名栈上存储的是对象的[内存地址],内容存储在堆上面
使用“==”是判断地址是否相同
7. ES6新增的特性了解和使用有多少?
- let关键字:
相较于var关键字的特点:
①变量不允许被重复定义
②不会进行变量声明提升
③保留块级作用域中- const: 定义常量的特点:①常量值不允许被改变 ②不会进行变量声明提升
- 解构赋值:
1️⃣数组解构
2️⃣对象解构- 字符串模板``
- 箭头函数:
与普通函数的区别:
书写上用=>代替了function;
普通函数的this指向window,而ES6箭头函数本身没有this,箭头函数的this指向父级作用域的this;
箭头函数的特点:
不需要 function 关键字来创建函数;
省略 return 关键字;
ES6中,箭头函数本身没有this,箭头函数继承父级作用域的 this;
ES6中,箭头函数不存在arguments对象;- 数组新增的方法:
Array.from():
遍历数组元素,也可遍历类似数组的兑现和可遍历的对象;
Array.of():
将一组值,转换为数组;例如:Array.of(3,12,5) ; //[3,12,5]
;
Array.find():
找出第一个符合条件的数组成员,他的参数是一个回调函数,所有数组成员依次执行该函数,知道找出第一个返回值为true
的成员,探后返回该成员,如果没有符合条件的成员,则返回undefined
;例如:[1,3,5,10].find( (n) => n>5)
;
Array.findIndex(当前的值,当前的位置,原数组):
用法与find方法非常相似,返回第一歌符合条件的数组成员的位置,如果所有成员都不符合条件则返回-1;
Array.keys():
对数组键名的遍历,返回一个遍历器对象,供for ...of 循环进行遍历;例如:for (let index of ['a','b'].keys() ) { console.log(index); } //0 //1
;
Array.values():
对数组键值的遍历,返回一个遍历器对象,供for ...of 循环进行遍历;例如:for (let elem of ["a","b"].values() ){ console.log(elem); } //'a' //'b'
;
Array.entries() :
对数组值对的遍历,返回一个遍历器对象,供for ...of 循环进行遍历;例如:for ( let [index,elem] of ['a','b'].entries() ) { console.log(index,elem); } //0 "a" // 1 "b"
;
Array.includes():
在没有这个方法之前,通常是用indexOf()检测是否包含某个数组,indexOf方法的缺点是:不够语义化,没有使用 ===严格相等这样会出现NaN的误判。[NaN].indexOf(NaN) //-1 [NaN].includes(NaN) //true [1,2,3].includes(2)//true
Array.fill():
使用给定值,填充一个数组,并且fill方法还可以接收第二个和第三个参数,用于指定填充的起始位置和结束位置;例如:['a','b','c'].fill(6,1,2) //['a',6,'c']
;
数组的扩展参考:https://www.imooc.com/article/20567- 对象新增的方法:
Object.is(value1,value2):
判断两个参数是否相等;
Object.assign(target,source):
用于对象的合并,将源对象source
的所有可枚举属性,复制到目标对象target
,也是浅拷贝:Object.assign({},obj)
;
扩展运算符:
{a,b,…c}={a:'a',b:'b',c:'c',d:'d'}→c={c:'c',d:'d'}
;
for…in...:
循环遍历出对象返回键名key
;
Object.keys(obj):
返回保存键名的字符串数组['a','b']
,作为遍历一个对象的补充手段,供for...of...
循环使用;
Object.values(obj):
返回保存键值的数组[1,2]
,作为遍历一个对象的补充手段,供for...of...
循环使用;
Object.entries(obj):
返回保存键值对的数组[['a',1],['b',2]]
,作为遍历一个对象的补充手段,供for...of...
循环使用;
Object.setPrototypeOf():
该方法的作用与_proto_
相同,用来设置对象的prototype对象,返回参数对象本身,他是ES6正式推荐的设置原型对象的方法Object.setPrototypeOf(obj, proto);
;
Object.getPrototypeOf():
与Object.setPrototypeOf
方法配套,用于读取一个对象的原型对象Object.getPrototypeOf(obj);
;
getOwnPropertyDescriptors():
此方法增强了ES5中getOwnPropertyDescriptor()
方法,可以获取指定对象所有自身属性的描述对象。结合defineProperties()
方法,可以完美复制对象,包括复制get和set属性;
对象的扩展参考:https://www.imooc.com/article/20569- Symbol用法:这是一种数据类型,表示独一无二的值;
Symbol参考:https://www.imooc.com/article/20574- ES6提供了Set和Map的数据结构:
Set,本质与数组类似。不同在于Set中只能保存不同元素,如果元素相同会被忽略。且由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值)。
Set实例的属性和方法:
Set结构的实例有属性:
Set.prototype.constructor:构造函数,默认就是Set函数;
Set.prototype.size:返回Set实例的成员总数;
Set实例的方法分为两类:操作方法和遍历方法;
Set操作方法:
add(value):添加某个值,返回Set结构本身
delete(value):删除某个值,返回一个布尔值,表示删除是否成功;
has(value):返回一个布尔值,表示该值是否为Set的成员;
clear():清除所有成员,没有返回值;
Set遍历操作:
Set结构的实例有四个遍历方法,可以用于遍历成员
keys():返回键名的遍历器
values():返回键值的遍历器
entries():返回键值对的遍历器
forEach() :使用回调函数遍历每个成员
(1)keys(),values(),entries()// 接收数组 let set2 = new Set([12,13,14,15,15]);// 得到[12,13,14,15] set.add(1) //[1,12,13,14,15] set.size //5 set.has(14) //true set.delete(1) //[12,13,14,15] set.clear() //undefined
Map,本质是与Object类似的结构。不同在于,Object强制规定key只能是字符串。而Map结构的key可以是任意对象。即:
object是集合
map是let maap = new Map([['key1','value1'],['key2','value2']]) maap.keys() // MapIterator {"key1", "key2"} maap.values() // MapIterator {"value1", "value2"} maap.size // 2 maap.entries() // MapIterator {"key1" => "value1", "key2" => "value2"} maap.set('key3','value3') // Map(3) {"key1" => "value1", "key2" => "value2", "key3" => "value3"} maap.get('key3') // "value3" maap.delete('key2') // true maap.entries() // MapIterator {"key1" => "value1", "key3" => "value3"}
Set和Map数据结构参考:https://www.imooc.com/article/20578
- Promise
1️⃣什么是promise?
将异步任务同步化的执行方式;
2️⃣promise的基本使用?
通过new promise创建一个promise对象,里面是一个回调函数,回调函数中有2个参数resolve和reject,resolve()当异步执行成功的时候调用的方法,reject()当异步失败的时候调用的方法。
除此之外promise有then方法和catch方法,当成功的时候执行.then(),当失败的时候执行.cath()
.then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。
3️⃣Promise 的优势(特点)?
对象的状态不受外界影响;
就在于这个链式调用。我们可以在 then 方法中继续写 Promise 对象并返回,然后继续调用 then 来进行回调操作,成功用resolve 将异步操作的结果,作为参数传递出去,失败用reject返回;
promise状态一经改变不会再变;
4️⃣方法
.then() 异步操作成功时执行
.cath() 异步操作失败时执行
.all() 当所有的异步代码都执行完毕以后才会执行.then中的操作
.race() 只要有一个promise执行完毕后就会执行.then操作
5️⃣使用场景
主要用于异步计算
可以将异步操作队列化,按照期望的顺序执行,返回符合预期的结果
可以在对象之间传递和操作promise,帮助我们处理队列
封装API接口和异步操作var p1 = new Promise(function(resolve,reject){ setTimeout (function () { console.log('1') resolve() },3000) }) function p2 () { return new Promise(function (resolve,reject){ setTimeout(function(){ console.log('2') resolve() },2000) }) } function p3 () { return new Promise(function(resolve,reject){ setTimeout(function(){ console.log('3') resolve() },1000) }) } function p4() { return new Promise(function (resolve,reject){ setTimeout(function(){ console.log('4') resolve() },500) }) } p1.then(function(){ return p2() }).then(function(){ return p3() }).then(function(){ return p4() }) //1 //2 //3 //4
Promise语法参考:https://www.imooc.com/article/20580
- 类(class)
ES6中提供的类实际上只是JS原型模式包装,有了class,对象的创建,继承更直观,父类方法的调用,实例化,静态方法和构造函数更加形象化。//基本定义和生成实例 class Parent{ constructor(name){ this.name = name; } } let parent = new Parent(‘xiaomao’) //继承 class Child extends Parent{ //子类怎么去覆盖父类,this一定要放在super后面 constructor(name = ‘child’){ super(name); //若super(),则所有参数都是父类的 this.type = ‘child’; //子类增加的属性 } }
Class的基本语法参考:https://www.imooc.com/article/20596
- Generator函数
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
形式上,Generator 函数是一个普通函数,但是有两个特征。
⭐function关键字与函数名之间有一个星号;
⭐函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)
⭐执行yield的时候要配合next()使用// 使用 * 表示这是一个 Generator 函数 // 内部可以通过 yield 暂停代码 // 通过调用 next 恢复执行 function* helloWorldGenerator() { let a = 1 + 2; yield 2; yield 3; } var hw = helloWorldGenerator(); console.log(b.next()); // > { value: 2, done: false } console.log(b.next()); // > { value: 3, done: false } console.log(b.next()); // > { value: undefined, done: true } //上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(2和3),即该函数有两个状态:2,3(结束执行)。
Generator函数的语法参考:https://www.imooc.com/article/20585
Generator函数的异步应用参考:https://www.imooc.com/article/20594
参考原文链接:
https://www.cnblogs.com/houjl/p/10087687.html
https://blog.csdn.net/ChinaW_804/article/details/109601546
https://www.cnblogs.com/wasbg/p/11160415.html
8. == 和 === 的区别?
== 是相对比较; === 是绝对比较
== 表示相等 (值相等)
===表示恒等(类型和值都要相等)
js在比较的时候如果是 == 会先做类型转换,再判断值得大小,如果是===类型和值必须都相等
9. 什么是包装类?
js中提供了三种特殊的引用类型(String Number Boolean)每当我们给原始值赋属性值时 后台都会给我们偷偷转换 调用包装类
怎么进行“包装”的?var str="hello word"; //var str = new String("hello world"); // 1.创建出一个和基本类型值相同的对象 //var long = str.length; // 2.这个对象就可以调用包装对象下的方法,并且返回结给long变量 //str = null; // 3.之后这个临时创建的对象就被销毁了 var long=str.length; //因为str没有length属性 所以执行这步之前后台会自动执行以上三步操作 console.log(long); // (结果为:10) //var str = new String("hello word"); // 1.因为下面有输出创建出str.length 而str不应该具有length这个属性 所以再次开辟空间创建出一个和基本类型值相同的对象 //str.length=nudefined; // 2.因为包装对象下面没有length这个属性没有值,所以值是未定 //str = null; // 3.这个对象又被销毁了 console.log(str.length) // (结果为:undefined)
参考原文链接:https://blog.csdn.net/qq_41853863/article/details/81227734
10.防抖和节流的原理,手写一下?
使用场景和优点:
在进行窗口的onscroll,oninput,resize,onkeyup等操作时,如果事件处理函数调用的频率无限制,浏览器的负担会很大,形成卡顿等问题,用户体验会非常糟糕。防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行
⭐防抖(debounce):就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
例如:eg. 像仿百度搜索,就应该用防抖,当我连续不断输入时,不会发送请求;当我一段时间内不输入了,才会发送一次请求;如果小于这段时间继续输入的话,时间会重新计算,也不会发送请求。
适用场景:按钮提交场景:防止多次提交按钮,只执行最后提交的一次 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似;
// 防抖:(每次触发的事件都会在x秒后被执行) function debounce(fn,wait){ let timer =null; return function(){ let context = this; let args = arguments; clearTimeout(timer) timer = setTimeout(function(){ fn.apply(context,args); },wait) } } // 改良防抖:(第一次会立即触发,之后触发的事件会在延迟x秒被执行) function debounce(fn,wait){ let timer = null; //null转化成布尔值为false return function(){ let context = this; let args = arguments; //判断第一次触发:当timer为null时,是第一次触发; let firstTime = !timer; //!timer -> !null -> !false ->true //timer存在且为true,就会清楚定时器; if(timer) { clearTimeout(timer) } //第一次触发:firstTime为true,就会触发执行fn(); if (firstTime) { fn.apply(context,args); } //如果timer不存就开启一个定时器,在第一次触发后延迟wait秒,将timer置空,相当于延迟wait秒后重新执行第一次触发的逻辑语句 timer = setTimeout(function(){ timer = null; },wait) } } // 处理函数 function handle() { console.log(Math.random()); } // 滚动事件 window.addEventListener('scroll', debounce(handle, 1000)); //input事件 //function inputTap(e) { // console.log(e.target.value) // } // document.addEventListener('input', inputTap)
当持续触发scroll事件时,事件处理函数handle只在停止滚动1000毫秒之后才会调用一次,也就是说在持续触发scroll事件的过程中,事件处理函数handle一直没有执行。
- ⭐节流(throttle):就是指连续触发事件但是在 n 秒中只执行一次函数。节流会稀释函数的执行频率。
- 节流函数原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。
- 例:(连续不断动都需要调用时用,设一时间间隔),像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多。
适用场景:
拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
缩放场景:监控浏览器resize
动画场景:避免短时间内多次触发动画引起性能问题
// 手写节流 时间戳版 const throttle = function (fn, delay) { let preTime = Date.now() //或者时间戳 new Date().getTime() return function () { let context = this let args = arguments let curTime = Date.now() if (curTime - preTime >= delay) { //当时间间隔大过于传入的delay时,才开始执行 fn.apply(context , args ) preTime = Date.now() } } } // 手写节流 2 定时器版 const throttle2 = function (fn, delay) { let timer = null return function () { //let context = this //let args = arguments if (!timer) { timer = setTimeout(() => { fn.apply(this, arguments) //箭头函数的this本身就指向父级作用域的this,也就是 return function () {}作用域里的this clearTimeout(timer) timer = null // 没有这句话就废了 }, delay); } } } //手写节流3 时间戳+定时器 const throttle3 = function (fn, delay) { var timer = null; var preTime = Date.now(); return function () { var curTime = Date.now(); var remaining = delay - (curTime - preTime ); clearTimeout(timer); if (remaining <= 0) { fn(); preTime = Date.now(); } else { timer = setTimeout(fn, remaining); } } } function handle() { console.log(Math.random()); } window.addEventListener('scroll', throttle(handle, 1000));
参考:
https://blog.csdn.net/qq_36818627/article/details/108023381(专业版 防抖,节流)
https://www.cnblogs.com/magicg/p/12657349.html
https://www.cnblogs.com/xxxx0130/p/13591675.html
https://blog.csdn.net/weixin_45932733/article/details/111528937
11. 对象的深拷贝和浅拷贝
1️⃣为什么需要拷贝?对象在赋值时,赋的是引用地址,所以在改变对象内属性值后,引用对象的变量也会改变;
let a = { a:1 } let b = a; a.a = 3; console.log(b.a) // 3
2️⃣如何实现拷贝?(用不同的方式实现拷贝,用ES3,ES5,ES6)
⭐深拷贝和浅拷贝的本质区别:假设B复制了A,当修改A时,看B是否会发生变化,如果B也跟着变了,说明这是浅拷贝,拿人手短,如果B没变,那就是深拷贝,自食其力
- 浅拷贝:利用循环对象属性的方式
ES3: fo...in... 循环遍历出对象的键名
ES5:
ES5中有 Object.getOwnPropertyNames(obj) 相当于ES6中的 Object.keys(obj);
forEach + Object.getOwnPropertyNames(obj) 循环迭代对象的键名并赋值;
ES6:
for...of... + Object.keys(obj) 循环迭代对象的键名并赋值;
for...of... + Object.entries(obj) 循环迭代对象的键值对并赋值;
Object.assign()方法:将所有可枚举属性的值从一个或多个源对象复制到目标对象,它将返回目标对象;
ES6的解构赋值...;let obj = { a:1, //第一级属性 b:2, c: { d:4, //第二级属性 e:[1,2,3,4] } } // 浅拷贝 function simpleClone (obj) { let cloneObj = {}; /*********************************************************************/ // ES3: fo in for (var key in obj) { // key:键名 cloneObj[key] = obj[key]; } /*********************************************************************/ //ES6: for of :用于遍历可迭代的对象 console.log(Object.keys(obj)); //返回保存键名的字符串数组 ['a','b'] console.log(Object.values(obj)); //返回保存键值的数组 [1,2] console.log(Object.entries(obj)); //返回保存键值对的数组 [['a',1],['b',2]] for (let key of Object.keys(obj)) { cloneObj[key] = obj[key]; } for (let [key,value] of Object.entries(obj)) { cloneObj[key] = value; } /*********************************************************************/ //ES5: 有Object.getOwnPropertyNames(obj) 相当于ES6中的 Object.keys(obj) Object.getOwnPropertyNames(obj).forEach(function (key) { cloneObj[key] = obj[key]; }) return cloneObj; } /*********************************************************************/ // Object.assign:第一级属性深拷贝,以后级别属性浅拷贝 let obj1 = Object.assign({},obj) //返回的不是一个新对象,而是把一个或多个源对象添加到目标对象 //解构赋值:第一级属性深拷贝,以后级别属性浅拷贝 let obj2 = {...obj}; obj.a = 11; obj.b = 22; obj.c.d = 44; obj.c.e.push(5); console.log(simpleClone(obj)); //{a: 11, b: 22, c: { d: 44, f:[1,2,3,4,5]}} console.log(obj1); // {a: 1, b: 2, c: { d: 44, f:[1,2,3,4,5]}} console.log(obj2); // {a: 1, b: 2, c: { d: 44, f:[1,2,3,4,5]}}
- 深拷贝:
原生JS-递归遍历
JSON.parse 和 JSON.stringify
jquery 的 extend方法 (进入https://www.bootcdn.cn/搜索jQuery,引用jQuery)let obj = { a:1, //第一级子属性 b:{ c:3,//第二级子属性 d:{ e:5, //第三级子属性 f:[1,2,3,4,5,6] } } } // 深拷贝 function deepClone (obj,cloneInit) { // 源对象,初始值 let cloneObj = cloneInit || {}; /*********************************************************************/ //原生js-递归遍历:1.这个函数在做什么事情? 2.递归的出口在哪里?走else分支确定出口 for(let i in obj) { //遍历一级子属性 //判断obj一级子属性是否有对象,如果有,递归复制 if(typeof obj[i] === 'object' && obj[i] !== null) { //null转化成布尔值为'Object' cloneObj[i] = {}; for(let j in obj[i]) { //遍历二级子属性 if(typeof obj[i][j] === 'object' && obj[i][j] !== null) { //判断obj的二级子属性是否有对象,如果有,递归复制 cloneObj[i][j] = {}; for(let k in obj[i][j]) { //遍历三级子属性 //由于已知三级属性没有对象类型,所以简单复制 cloneObj[i][j][k] = obj[i][j][k] console.log(cloneObj[i][j][k]) //5 } }else{ //如果不是,简单复制 cloneObj[i][j] = obj[i][j] console.log(cloneObj[i][j]) //3 } } }else{ cloneObj[i] = obj[i] } } /*********************************************************************/ // 改造递归遍历:内部调用自己实现递归 + 数组的判断 for(let i in obj) { //遍历一级子属性 //判断obj一级子属性是否有对象,如果有,递归复制 if(typeof obj[i] === 'object' && obj[i] !== null) { //null转化成布尔值为'Object' //cloneObj[i] = {}; //数组的判断: cloneObj[i] = Array.isArray(obj[i]) ? [] : {}; // instanceof 只能检测引用类型,不能判断普通数据类型 cloneObj[i] = obj[i] instanceof Array ? [] : {}; //Object.prototype.toString.call() 将当前的this指向调用者,用过返回值判断 cloneObj[i] = Object.prototype.toString.call(obj[i]) === '[object,Array]' ? [] : {}; deepClone(obj[i],cloneObj[i]) // 源对象,初始值 }else{ //如果obj一级子属性没有对象,直接复制 cloneObj[i] = obj[i] } } /*********************************************************************/ // JSON.parse 和 JSON.stringify return JSON.parse(JSON.stringify(obj)) /*********************************************************************/ //jquery 的 extend方法 // 引用jQuery // console.log($.extend(true,{},obj)) //extend(是否深拷贝,目标对象,源对象) return $.extend(true,{},obj); return cloneObj; } let obj1 = deepClone(obj); obj.b.c = 10; obj.b.d.e = 100; obj.b.d.f.push(7) console.log(obj1);
12. 你对闭包的理解? 闭包使用场景?
- 闭包就是:可以在一个内层函数中访问到其外层函数的作用域。
创建闭包的最常见的方式就是在函数内创建函数,通过函数访问函数的局部变量,利用闭包可以突破作用链域。
举一个简单地:function init(){ var name ="Mozilla";// name 是一个被 init 创建的局部变量 function displayName(){// displayName() 是内部函数,一个闭包 alert(name);// 使用了父函数中声明的变量 } displayName(); } init() // displayName() 没有自己的局部变量。然而,由于闭包的特性,它可以访问到外部函数的变量
- 使用场景(离不开两个特点):
⭐创建私有变量
⭐延长变量的生命周期- 特性:
函数内再嵌套函数
内部函数可以引用外层的参数和变量
参数和变量不会被垃圾回收机制回收- 优缺点
优点是可以避免全局变量的污染,设计私有的方法和变量来实现封装
缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露,所以不能滥用闭包,退出函数之前,将不使用的局部变量全部删除。- 作用
让函数外部可以操作函数内部的数据
让这些变量始终保持在内存中,延长局部变量生命周期
封装对象的私有属性和私有方法
参考原文链接:
https://www.jianshu.com/p/ba4cbf7cbe60
https://www.jianshu.com/p/21b0bb221023
13.作用域和作用域链?
作用域:就是变量与函数的可访问范围。
作用域最大的用处:就是隔离变量,不同作用域下同名变量不会有冲突。
作用域分为:全局作用域,函数作用域,块集作用域。
全局作用域:编写在script标签中的js代码,都在全局作用域。全局作用域在页面打开时创建,在页面关闭时销毁。在全局作用域中有一个全局对象window,代表浏览器窗口,由浏览器创建,可以直接使用。我们创建的全局变量会作为window对象的属性保存,创建的方法都会作为window对象的方法保存
函数作用域:用函数时创建,函数执行完毕后销毁,每调用一次函数就会创建一个函数作用域,相互独立。在函数作用域中可以访问到全局作用域的变量,在全局作用域中不能访问到函数作用域的变量。当在函数作用域中操作某个变量时,先在自身作用域中寻找这个变量,没有的话再向上一级作用域找,直到找到全局作用域(也就是顺着作用域链找)如果在函数作用域里有和全局作用域里同名的变量,但是要访问全局作用域里的变量,可通过window对象调用
块集作用域:一般是通过let声明和const声明产生的变量,在所属的块的作用域外不能访问。块通常是一个被花括号包含的代码块。优点是使外部作用域不能访问内部作用域的变量,规避命名冲突
作用域链:当在函数作用域中操作某个变量时,先在自身作用域中寻找这个变量,没有的话再向上一级作用域找,直到找到全局作用域(也就是顺着作用域链找)若还是没找到,就宣布放弃。这种一层一层的关系,就是 作用域链 。
参考原文:https://www.jianshu.com/p/ba4cbf7cbe60
https://www.cnblogs.com/leftJS/p/11067908.html
14.JavaScript垃圾回收机制的了解?
JavaScript的解释器可以检测到什么时候程序不再使用这个对象了(数据),就会把它所占用的内存释放掉。
针对JavaScript的回收机制有两种方法(常用):标记清除,引用计数。
1️⃣标记清除:当变量进入到执行环境时,垃圾回收器就会将其标记为“进入环境”,当变量离开环境时,就会将其标记为“离开环境”。
2️⃣引用计数:引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变成0时,则说明没有办法再访问这个值了,因而就可以将其所占的内存空间给收回来。
⭐引用计数有个最大的问题: 循环引用。
参考原文:https://www.jianshu.com/p/fddd86d545b6
⭐内存泄漏:在某些情况下,不再使用到的变量所占用内存没有及时释放,导致程序运行中,内存越占越大,极端情况下可以导致系统崩溃
15.柯里化函数?
柯里化是指这样一个函数(假设叫做createCurry),他接收函数A作为参数,运行后能够返回一个新的函数。并且这个新的函数能够处理函数A的剩余参数。
柯里化的目的:在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用。// 假设我们有一个求长方形面积的函数 function getArea(width, height){ return width * height } // 如果我们碰到的长方形的宽老是10 const area1 = getArea(10,20) const area2 = getArea(10,30) const area3 = getArea(10,40) // 我们可以使用闭包柯里化这个计算面积的函数 function getArea(width){ return height=>{ return width * height } } const getTenWidthArea = getArea(10) // 之后碰到宽度为10的长方形就可以这样计算面积 const area1 = getTenWidthArea(20) // 而且如果遇到宽度偶尔变化也可以轻松复用 const getTwentyWidthArea = getArea(20)
柯里化函数的运行过程:其实是一个参数的收集过程,我们将每一次传入的参数收集起来,并在最里层里面处理。
柯里化确实是把简答的问题复杂化了,但是复杂化的同时,我们使用函数拥有了
更加多的自由度。而这里对于函数参数的自由处理,正是柯里化的核心所在。举个:我们还可能会遇到验证身份证号,验证密码等各种验证信息。因此在实践中,为了统一逻辑,我们就会封装一个更为通用的函数,将用于验证的正则与将要被验证的字符串作为参数传入。
function check(targetString, reg) { return reg.test(targetString); }
但是这样封装之后,在使用时又会稍微麻烦一点,因为会总是输入一串正则,这样就导致了使用时的效率低下。
check(/^1[34578]\d{9}$/, '14900000088'); check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/, '[email protected]');
这个时候,我们就可以借助柯里化,在check的基础上再做一层封装,以简化使用。
//封装如下: // 简单实现,参数只能从右到左传递 function createCurry(func, args) { var arity = func.length; var args = args || []; return function() { var _args = [].slice.call(arguments); [].push.apply(_args, args); // 如果参数个数小于最初的func.length,则递归调用,继续收集参数 if (_args.length < arity) { return createCurry.call(this, func, _args); } // 参数收集完毕,则执行func return func.apply(this, _args); } } //这个createCurry函数的封装借助闭包与递归,实现了一个参数收集,并在收集完毕之后执行所有参数的一个过程。 var _check = createCurry(check); var checkPhone = _check(/^1[34578]\d{9}$/); var checkEmail = _check(/^(\w)+(\.\w+)*@(\w)+((\.\w+)+)$/);
最后在使用的时候就会变得更加直观与简洁了。
checkPhone('183888888'); checkEmail('[email protected]');
经过这个过程我们发现,柯里化能够应对更加复杂的逻辑封装。当情况变得多变,柯里化依然能够应付自如。
参考原文:
https://www.jianshu.com/p/2975c25e4d71
https://www.jianshu.com/p/5e1899fe7d6b
https://www.jianshu.com/p/21b0bb221023
16. 构造函数和普通函数的区别?
1️⃣首字母大写
2️⃣可以通过new 关键字的方式执行。new的方式:是指在函数内部能够将this指向实例之后的对象
3️⃣构造函数通过new关键字执行后,函数内部的this就会构造函数创建的实例对象,也就是foo。所以在实例上就挂了a等于1的属性。function Foo () { this.a = 1; } let foo = new Foo(); console.log(foo);
17.JavaScript原型(其实就是原型对象),原型链 ? 有什么特点?
⭐原型对象是所有实例的公共祖先。
⭐所有实例可以拿到原型对象上的公共属性和方法。
⭐ptototype,-proto-,constructor之间的三角恋:
ptototype,-proto-都指向的是原型;
prototype:通过构造函数的方式指向原型;Foo.prototype
-proto-:通过实例的方式指向原型;foo.-proto-
constructor:原型内置的一个属性,原型的constructor属性指向都是构造函数本身。function Foo () { this.a = 1; } // 重新定义原型 Foo.prototype = { aa:10, bb:function () { console.log('20') }, //constructor:Foo 重写原型对象,会导致原型对象的 constructor 属性指向 Object ,导致原型链关系混乱,所以我们应该在重写原型对象的时候指定 constructor( instanceof 仍然会返回正确的值) } let foo = new Foo(); console.log(foo.aa); //10 foo.bb() //20 console.log(foo) console.log(Foo.prototype === foo.__proto__) //true console.log(Object.prototype === foo.__proto__.__proto__) //true //没有重新定义原型之前: console.log(Foo.prototype.constructor === Foo) //true console.log(foo.constructor === Foo ) //true 因为实例可以访问原型上的属性和方法 //但是,在重新定义原型后: // 由于没有写constructor:Foo这句话(意思是在实例的原型上定义构造函数),所以在实例的原型中没有找到构造函数就会向原型的原型中查找,最终在原型的原型中找到构造函数Object并返回; console.log(foo.constructor ) //Object 因为我们重写原型后,并没有定义构造函数,所以它会沿着原型链继续查找,拿到父级的父级中的构造函数Object;
不推荐使用实例的方式访问原型(foo.-proto-),一般情况下推荐使用构造函数的方式访问(Foo.prototype),如果非要通过实例的方式访问原型,可以使用ES6中的Object.getPrototypeOf(foo)
console.log(Foo.prototype === Object.getPrototypeOf(foo)) //true
⭐原型链:简单理解就是原型组成的链,每个对象(实例对象 new Preson)都有一个proto属性指向它的原型,而原型也是一个对象,也有proto属性,原型的proto又指向它自己的原型,就这样可以一直通过proto向上找,这就是原型链,当向上找到Object的原型的时候,这条原型链就算到头了。
参考原文:
知乎:https://zhuanlan.zhihu.com/p/62903507
https://www.imooc.com/article/275399?block_id=tuijian_wz
https://blog.csdn.net/haitunmin/article/details/78418656
18. 对象的继承
对象的继承:对象的继承只能继承自身原型上的属性和方法;
假设:我们想通过sub1获取到Super构造函数和它原型上的属性和方法,这时候我们就需要用到对象的继承。
原型链继承:由于对象继承只能继承自身原型上的属性和方法,所以我们通过改变原型链来实现原型链继承。
- 原型链继承 : 会出现引用值共享的问题;
- 构造函数继承:解决原型链继承中引用值共享的问题,无法调用原型上的方法;
- 组合式继承(伪金典继承):解决引用值共享的问题和无法调用原型上的方法的问题,但‘伪’又造成了构造函数复用的问题;
- 寄生组合继承(金典继承):通过Object.create() 创建了父类原型的副本,与父类原型完全隔离,解决伪金典继承中构造函数复用的问题;
- 圣杯布局:主要思想是利用一个临时函数作为中间层以及原型链的方式实现继承;将子对象的 prototype 指向父对象的 prototype;
- 拷贝继承:如果把父对象的所有属性和方法,拷贝进子对象;
- ES6 Class通过 extends 关键字实现继承;
1️⃣原型链继承
//父类构造函数 function Super () { //this.a = '111'; this.a = [1,2,3,4]; } Super.prototype.say = function () { console.log(222) } //子类构造函数 function Sub () { } // 通过原型链继承:由于sub1只能拿到自身原型上的属性和方法,所以将原型指向另一个对象的实例,让对象的实例自身去继承原型相关的属性; Sub.prototype = new Super(); //所以我们现在的原型链是:sub1 -> Sub.prototype(new Super) -> Super.prototype // let sub1 = new Sub(); // console.log(sub1.a) //111 // sub1.say() //222 // 原型链继承存在弊端:引用值共享的问题; let sub1 = new Sub(); let sub2 = new Sub(); //此时修改sub1中属性a的值 sub1.a = '333'; console.log(sub1.a) //333 console.log(sub2.a) //111 //从上面的结果看不出任何问题,但是,如果Super构造函数中保存的属性a是引用类型值,a=[1,2,3,4] sub1.a.push(5); console.log(sub1.a) // [1,2,3,4,5] console.log(sub2.a) // [1,2,3,4,5]
由于原型链继承的特性,所以并不能保证引用类型的原始值不被修改,但能用构造函数继承解决引用值共享的问题;
2️⃣构造函数继承:解决原型链继承中引用值共享的问题;
用.call()和.apply()将父类构造函数引入子类函数,等于复制父类的实例属性给子类;创建子类实例时,可以向父类传递参数,可以实现多继承,可以方便的继承父类型的属性;//父类构造函数 function Super () { //this.a = '111'; this.a = [1,2,3,4]; } Super.prototype.say = function () { console.log(222) } //子类构造函数 function Sub () { Super.call(this) //将Super内部的this指向Sub创建的实例对象 } // 原型链继承存在弊端:引用值共享的问题; let sub1 = new Sub(); let sub2 = new Sub(); //此时修改sub1中属性a的值 sub1.a = '333'; console.log(sub1.a) //333 console.log(sub2.a) // [1,2,3,4] // 以上结果,更改a的值不会有影响 sub1.a.push(5); console.log(sub1.a) // [1,2,3,4,5] console.log(sub2.a) // [1,2,3,4] // 以上结果,用push的方式更改a的值也不会有影响 // 构造函数继承的弊端:无法调用say方法,也就是无法调用原型上的方法;
虽然构造函数通过改变this的指向解决了引用值共享的问题,但是却无法调用原型上的方法;
3️⃣组合式继承(伪金典继承):解决引用值共享的问题和无法调用原型上的方法的问题;//父类构造函数 function Super () { //this.a = '111' this.a = [1,2,3,4] } Super.prototype.say = function () { console.log(222) } //子类构造函数 function Sub () { // 构造函数继承 Super.call(this) } // 原型链继承 Sub.prototype = new Super() let sub1 = new Sub() let sub2 = new Sub() sub.a = '333' console.log(sub1.a) // 333 console.log(sub2.a) // [1,2,3,4] // 以上结果,普通赋值去更改a的值不会有影响 sub1.a.push(5) console.log(sub1.a) // [1,2,3,4,5] console.log(sub2.a) // [1,2,3,4] // 以上结果,用push的方式更改a的值也不会有影响 sub1.say() // 222 sub2.say() // 222 // 以上结果,调用原型上的方法也不会有影响
既然伪金典继承把原型链继承和构造函数继承结合的这么好,那么为什么还是‘伪’呢?
那伪金典到底‘伪’在哪呢,就‘伪’在他当前调用了两次Super实例,所以你看到的Super实际上执行了两次,我们通过指定原型链的方式继承Super执行了一次,然后在new Sub() 的时候,也就是在Sub中执行.call()的时候,变相的在执行了一次。
伪金典继承的弊端:构造函数复用的问题;
4️⃣寄生组合继承(金典继承):解决伪金典继承中构造函数复用的问题;//父类构造函数 function Super () { //this.a = '111' this.a = [1,2,3,4] } Super.prototype.say = function () { console.log(222) } //子类构造函数 function Sub () { // 构造函数继承 Super.call(this) } // Sub.prototype = new Super() // 我们希望Sub的原型== Super的实例,也就是能直接获取到Super原型身上的方法 // Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__ if(!Object.create){// 兼容方案,如果Object.create这个不存在 Object.create = function (proto) { //这里传入的proto是一个对象 function F () { } F.prototype = proto return new F() } } // Sub.prototype.subSay = function () { //如果Sub原型的方法写在重新定义Sub.prototype之前,该方法就会被覆盖 // console.log(444) //} //Object.create是创建了父类原型的副本,与父类原型完全隔离 Sub.prototype = Object.create(Super.prototype) //这是ES5的方法,所以需要些兼容方案 //由于上面我们重写了Sub.prototype的原型,导致经典继承拿不到Sub原型上的方法,但将自定义的subSay放法写在 重定义Sub.prototype的原型之后就能拿到; Sub.prototype.subSay = function () { //如果Sub原型的方法写在重新定义Sub.prototype之前,该方法就会被覆盖 console.log(444) } // 注意记得把子类的构造指向子类本身 Sub.prototype.constructor = Super; let sub1 = new Sub() let sub2 = new Sub() sub1.a = '333' console.log(sub1.a) // 333 console.log(sub2.a) // [1,2,3,4] // 以上结果,普通赋值去更改a的值不会有影响 sub1.a.push(5) console.log(sub1.a) // [1,2,3,4,5] console.log(sub2.a) // [1,2,3,4] // 以上结果,用push的方式更改a的值也不会有影响 sub1.say() // 222 sub2.say() // 222 // 以上结果,调用原型上的方法也不会有影响
5️⃣圣杯布局
主要思想是利用一个临时函数作为中间层以及原型链的方式实现继承。//第一种方法 function inherit( Target , Origin){ function F(){}; F.prototype = Origin.prototype; //不能与上边一行互换位置 Target.prototype = new F(); //要让Target的构造函数constructor归位 Target.prototype.constructor = Target; //使构造出的对象能够找到自己超级父级是谁,就是真正继承于谁 Target.prototype.uber= Origin.prototype; } /***********************************************************************************/ // //第二种使用立即执行函数 var inherit = (function(){ //声明局部变量F var F =function (){} ; //返回的匿名函数使用上边局部变量F,形成闭包,返回给inherit return function(Target,Origin){ F.prototype = Origin.prototype; Target.prototype = new F(); //要让Target的构造函数constructor归位 Target.prototype.constructor = Target; //使构造出的对象能够找到自己超级父级是谁,就是真正继承于谁 Target.prototype.uber = Origin.prototype; } }())
6️⃣拷贝继承:如果把父对象的所有属性和方法,拷贝进子对象
function extend(Child, Parent) { var p = Parent.prototype; var c = Child.prototype; for (var i in p) { c[i] = p[i]; } c.uber = p; }
7️⃣ES6 Class通过 extends 关键字实现继承
class 实现继承的核心在于使用 extends 表明继承自哪个父类,并在子类构造函数中必须调用 superclass Super { constructor(value) { this.a= value; } say() { console.log(this.a); } } class Sub extends Super{ constructor(value) { super(value); // // 调用父类的constructor(value) //this.a = value; } subSay() { console.log(this.a); } } let sub1 = new Sub([1,2,3,4]); let sub2 = new Sub([1,2,3,4]); sub1.a.push(5) console.log(sub1.a) //[1,2,3,4,5] console.log(sub2.a) // [1,2,3,4] sub1.say() //[1,2,3,4,5] sub2.say() // [1,2,3,4] sub1.subSay() //[1,2,3,4,5] sub2.subSay() // [1,2,3,4] sub1 instanceof Super // true
19. ES5和ES6继承的区别
- ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上Parent.apply(this)
- ES6的继承机制实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this,使得父类的所有行为都可以继承。
- ES5的继承时通过原型或构造函数机制来实现。
- ES6通过class关键字定义类,有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。
参考原文:https://www.jianshu.com/p/ba4cbf7cbe60
20. this指向
- 默认绑定规则:this指向window;
⭐默认绑定:函数独立调用时,this指向window,立即执行函数也属于函数独立调用;
⭐默认绑定:严格模式下this指向undefined,非严格模式this指向window- 隐式绑定规则:指对象调用, obj.foo() 谁调用this就指向谁;(隐式丢失,参数赋值)
- 显示绑定规则:call,apply,bind;
- new 绑定:this指向实例创建后的对象;
- 箭头函数绑定:箭头函数本身是不存在this的,箭头函数的this是父作用域的this,不是调用时的this,其他方法的this是动态的,而箭头函数的this是静态的;
优先级:箭头函数>new绑定>显示绑定/apply/bind/call>隐式绑定>默认绑定var obj = { a: 1, foo: function (){ console.log(this) //函数独立调用 function test () { console.log(this) } test() //函数独立调用 (function () {console.log(this)})() //window // 闭包:当函数执行的时候,导致函数被定义,并抛出 function fn () { console.log(this) //window } return fn; } } obj.foo(); // 闭包调用: obj.foo()(); // 相当于fn(); 就相当于函数独立调用 /**********************************************************************/ // 隐式丢失:(相当于参数赋值) function foo () { console.log(this) } var obj = { a:1, foo:foo } obj.foo(); //obj var bar = obj.foo; //相当于参数赋值,var bar = function foo () {console.log(this)} bar(); //window var bar = foo; bar(); //window /**********************************************************************/ // 参数(形参)赋值: function foo () { console.log(this) //window } // bar-父函数,fn-形参(子函数)或(回调函数) function bar (fn) { fn(); //function foo () {console.log(this)} 相当于函数独立调用 // 如果想要this指向obj,可以有多种方法改变this指向 fn.call(obj); //fn.apply(obj); //fn.bind(obj)(); } // 父函数有能力决定 子函数this 的指向 var obj = { a:2, foo:foo } bar(obj.foo) /**********************************************************************/ // new 绑定: function Person () { //let this = {}; //this.a = 1; //return this; // 这里的this指的是当前实例化后的对象(person) //return 1; //这里的this指向实例创建后的person return {}; //如果返回引用类型,this指向的是空对象{} } let person = new Person(); console.log(person); /**********************************************************************/ // 箭头函数: window.name='a' const obj={ name:'b', age:22, getName:()=>{ console.log(this) console.log(this.name) }, getAge:function(){ setTimeout(()=>{ console.log(this.age) }) } } obj.getName();//window a 箭头函数的this指向父级作用域的this,不是调用它的this obj.getAge();//22 /**********************************************************************/ // 练习: var name = 'window'; var obj1 = { name:'1', fn1:function () { console.log(this.name) }, fn2: () => console.log(this.name), fn3:function () { return function () { console.log(this.name) } }, fn4:function () { return () => console.log(this.name) } } var obj2 = { name:'2' } obj1.fn1(); // 1 对象调用,谁调用就指向谁 obj1.fn1.call(obj2); // 2 显示绑定规则 > 隐式绑定中的对象调用 obj1.fn2(); // window 箭头函数不存在this,直接找父作用域里的this obj1.fn2.call(obj2); // window 箭头函数的优先级最高,显示绑定无法改变箭头函数的this规则 obj1.fn3()(); // window 返回一个普通函数,这个函数独立调用 obj1.fn3().call(obj2); // 2 普通函数通过显示绑定改变this指向 obj1.fn3.call(obj2)(); // window 执行obj1.fn3.call(obj2) 使父函数通过显示绑定规则改变this指向,但子函数独立调用,所以是window obj1.fn4()(); // 1 父函数对象调用,this指向obj1,子函数是箭头函数且独自调用,this指向父级作用域 obj1.fn4().call(obj2); // 1 父函数对象调用,this指向obj1,但子函数是箭头函数,由于箭头函数优先级最高,显示绑定不能改变this规则 obj1.fn4.call(obj2)(); // 2 执行obj1.fn4.call(obj2) 使父函数this指向为obj2,子函数是箭头函数且独自执行,所以指向父作用域this
21. call、apply、bind
- 显示绑定:call、apply、bind都可以改变this的指向,但是apply接收参数数组,call接收的是参数列表 bind接收的是参数列表,但是apply和call调用就执行,bind需要手动执行;
function foo (a,b,c,d) { console.log(a,b,c,d) console.log(this) } var obj = { a:3, foo:foo } // 隐式丢失:变量赋值 var bar = obj.foo; obj.foo(1,2,3,4); // obj 1,2,3,4 bar.call(obj,1,2,3,4); // obj 1,2,3,4 bar.apply(obj,[1,2,3,4]); // obj 1,2,3,4 bar.bind(obj)(1,2,3,4); // obj 1,2,3,4