- 1. 请问在JS中有哪些数据类型
- 2. 数据类型检测的方式有哪些
- 3. 判断数组的方式有哪些
- 4. null和undefined区别
- 5. == 操作符的强制类型转换规则?
- 6. 其他值到字符串的转换规则?
- 7. 其他值到数字值的转换规则?
- 8. || 和 && 操作符的返回值?
- 9. 讲一讲JavaScript的装箱和拆箱?
- 10. 请问ES6新增的Symbol数据类型有何特点?
- 11. new操作符的实现原理
- 12. map和Object的区别
- 13. map和weakMap的区别
- 14. 对JSON的理解
- 15. JavaScript脚本延迟加载的方式有哪些?
- 16. JavaScript 类数组对象的定义?
- 17. 数组有哪些方法?
- 18. 什么是 DOM 和 BOM?
- 19. 对AJAX的理解,实现一个AJAX请求
- 20. JavaScript为什么要进行变量提升,它导致了什么问题?
- 21. 什么是尾调用,使用尾调用有什么好处?
- 22. 请问你了解js模块化吗?
- 23. 常见的DOM操作有哪些
- 24. 数组的遍历方法有哪些
- 25. 请问js有哪些继承方式
- 27. 防抖与节流
- 28. 请问js有哪些数组去重方法?
- 29. 请问你了解js事件循环机制(Event Loop)吗
- 30. 请问什么是事件流?
- 31. 请问什么是事件委托/事件代理?
- 32. 请问什么是事件监听?
- 33. 请问如何阻止事件触发?
- 34. Object.defineProperty(target, key, options),options可传什么参数?
- 35. 什么是函数柯里化?
- 36. 对原型、原型链的理解
- 37. 闭包
- 38. 对作用域、作用域链的理解
- 39. 对执行上下文的理解
- 40. this/call/apply/bind
- 41. 浏览器的垃圾回收机制
- 42. 请问js有哪几种常见的内存泄露情况?
- 43. let、const、var的区别
- 44. 箭头函数
- 45. 扩展运算符的作用及使用场景
- 46. Proxy 可以实现什么功能?
- 47. ES6有哪些新特性
- 48. 请问ES6 class与ES5构造函数有什么联系?
- 49. 迭代器与生成器
- 50. 请问什么是Reflect ?
- 51. 异步编程的实现方式?
- 52. setTimeout、Promise、Async/Await 的区别
- 53. 对Promise的理解
- 54. Promise的基本用法
- 55. 对async/await 的理解
- 56. await 到底在等啥
1. 请问在JS中有哪些数据类型
JS数据类型一共有8种,分为基本数据类型和引用数据类型
基本数据类型:字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、Symbol、BigInt
Symbol:ES6引入了一种新的原始数据类型,表示独一无二的值,主要用于解决属性名冲突的问题,做为标记
BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
引用数据类型:对象(Object),其中包含了日期(Date)、函数(Function)、数组(Array)、正则(RegExp)等
两者总结区别:
(1)声明变量时不同的内存分配:
基本:存储在栈中的简单数据段,它们的值直接存储在变量访问的位置
原因:基本类型数据占据的空间是固定的,所以将他们存储在较小的内存区域——栈,便于迅速查寻变量的值
引用:存储在堆中的对象,存储在变量处的值是一个指针,指向存储对象的内存地址
原因:引用类型数据的大小会改变,不能把它放在栈中,否则会降低变量查寻速度,相反,地址的大小是固定的,可以存在栈中
(2)不同的内存分配机制也带来了不同的访问机制
引用:js中不允许直接访问保存在堆内存中的对象,在访问一个对象时,首先得到对象在栈内存中的地址,按照这个地址去获得对象中的值(引用访问)
基本:可直接访问
(3)复制变量时的不同
基本:变量复制时,会将原始值的副本赋值给新变量,此后两变量是完全独立的(修改一个不会影响另一个),他们只是拥有相同的值而已(深拷贝)
引用:变量复制时,会把内存地址赋值给新变量,新旧变量都指向了堆内存中的同一个对象,任何一个作出的改变都会影响另一个(浅拷贝)
(4)参数传递的不同(把实参复制给形参的过程)
由于内存分配的差别,两者在传参时也有区别
基本:只是把变量里的值传递给参数,之后参数和这个变量互不影响
引用:传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部,因为它们都指向同一个对象。
备注:
原始值(undefined、null、布尔值、数值、字符串)是不可修改的,是按值比较的,值相等就相等;
对象当且仅当它们引用同一个底层对象时才是相等的。
2. 数据类型检测的方式有哪些
- typeof xx
返回一个字符串(小写),用来判断:Undefined、String、Number、Boolean、Symbol、Object、Function,无法检测其它引用类型。
优点:可区分Object与Function
缺点:
(1)对于 Null ,返回 object 类型
原因:Null类型只有一个null值,该值表示一个空对象指针(出自JavaScript高级程序设计)
typeof的检测原理:不同的对象在底层都表示为二进制,在js中二进制前(低)三位存储其类型信息为:000: Object、100:String、110: Boolean、1: Number。null的二进制表示全为0,自然前三位也是0,所以执行typeof时会返回”object”。
(2) 对于Array、Date、RegExp都会返回object,不能更详细的区分
console.log(typeof '12');// string
console.log(typeof 12);// number
console.log(typeof undefined);// underfined
console.log(typeof true);// boolean
console.log(typeof null);// object
- xx instanceof xx
返回true/false,只能判断引用类型 ,无法检测基本类型
判断原理:判断一个构造函数的prototype是否存在另外一个要检测对象的原型链上。简单来说:能验证new构造函数创建出来的实例,左边的对象是否是右边的类的实例,属于验证式判断类型
缺点:只能用来判断两个对象是否属于实例关系, 而不能判断一个对象实例具体属于哪种类型(原型链上的都会返回true)
console.log('abc' instanceof String);// false
console.log(String('abc') instanceof String);// true
console.log(12 instanceof Number);// false
console.log(new Number(12) instanceof Number);// true
console.log(true instanceof Boolean);// false
console.log(new Boolean(true) instanceof Boolean);// true
console.log({name:'yy'} instanceof Object);// true
console.log(new Object({name:'yy'}) instanceof Object);// true
console.log(['12','123'] instanceof Object);// true
console.log(['12','123'] instanceof Array);// true
console.log(new Array('12',32) instanceof Object);// true
console.log(new Array('12',32) instanceof Array);// true
console.log(function(){} instanceof Object);// true
console.log(function(){} instanceof Function);// true
console.log(new Function() instanceof Function);// true
console.log(new Date() instanceof Object);// true
console.log(new RegExp instanceof Object);// true
console.log(new String('abc') instanceof Object);// true
console.log(new Number(12) instanceof Object);// true
实现:
function myInstanceof(left, right) {
// 获取对象的原型
let proto = Object.getPrototypeOf(left)
// 获取构造函数的 prototype 对象
let prototype = right.prototype;
// 判断构造函数的 prototype 对象是否在对象的原型链上
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
// 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
proto = Object.getPrototypeOf(proto);
}
}
- xx.constructor === xx
返回true/false,判断原理:
p.constructor === Person.prototype.constructor
当一个函数F被定义时,JS引擎会为F添加prototype原型,然后再在prototype上添加一个constructor属性,并让其指向F的引用
具体来说:当 var f = new F() 时,F被当成了构造函数,f是F的实例对象,此时F原型上的constructor传递到了f上,因此f.constructor === F
缺点:不可判断Null、Undefined是无效的对象,没有constructor存在
constructor 是不稳定的,如创建的对象更改了原型,无法检测到最初的类型
console.log(''.constructor === String);//true
console.log(new Number(1).constructor === Number);//true
console.log([].constructor === Array);//true
console.log(true.constructor === Boolean);//true
console.log(new Function().constructor === Function;);//true
console.log(new Date().constructor === Date);//true
console.log(document.constructor === HTMLDocument);//true
- Object.prototype.toString.call(xx)
返回“[object type]”(字符串),能判断所有类型,万金油方法
判断原理:JS中的所有对象都是继承自Object对象的,通过call方法(显式绑定)改变this指向,利用Object.prototype上的原生toString()方法判断数据类型。
同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?
这是因为toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。
console.log(Object.prototype.toString.call(123));//[object Number]
console.log(Object.prototype.toString.call('123'));//[object String]
console.log(Object.prototype.toString.call(undefined));//[object Undefined]
console.log(Object.prototype.toString.call(true));//[object Boolean]
console.log(Object.prototype.toString.call({}));//[object Object]
console.log(Object.prototype.toString.call([]));//[object Array]
console.log(Object.prototype.toString.call(function(){}));//[object Function]
3. 判断数组的方式有哪些
// 1. 通过Object.prototype.toString.call()做判断
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
// 2. 通过原型链做判断
obj.__proto__ === Array.prototype;
// 3. 通过ES6的Array.isArray()做判断
Array.isArrray(obj);
// 4. 通过instanceof做判断
obj instanceof Array
// 5. 通过Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)
4. null和undefined区别
共同点:都是基本类型,保存在栈中;转布尔值都是false;null == undefined 为 true
不同点:
Undefined:表示”缺少值”,就是此处应该有一个值,但是还没有定义,转为数值时为NaN。
典型用法:
- 变量被声明了,但没有赋值时,就等于undefined
- 函数定义了形参,但没有传递实参
- 对象没有赋值的属性,该属性的值为undefined
- 函数没有返回值时,默认返回undefined
Null:null 的字面意思是:空值 。这个值的语义是,希望表示一个对象被人为重置为空对象。 在内存里的表示就是,栈中的变量没有指向堆中的内存对象(空指针)。
注意:
undefined == null; //true
undefined === null; //false
Number(undefined); // NaN
Number(null); // 0
5. == 操作符的强制类型转换规则?
对于 == 来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 x 和 y 是否相同,就会进行如下判断流程:
- 首先会判断两者类型是否相同,相同的话就比较两者的大小;
- 类型不相同的话,就会进行类型转换;
- 会先判断是否在对比 null 和 undefined,是的话就会返回 true
- 判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number
- 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
- 判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断
6. 其他值到字符串的转换规则?
- Null 和 Undefined 类型 ,null 转换为 “null”,undefined 转换为 “undefined”,
- Boolean 类型,true 转换为 “true”,false 转换为 “false”。
- Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
- Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。
对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString()(Object.prototype.toString())来返回内部属性 [[Class]] 的值,如”[object Object]”。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。
7. 其他值到数字值的转换规则?
Undefined 类型的值转换为 NaN。
- Null 类型的值转换为 0。
- Boolean 类型的值,true 转换为 1,false 转换为 0。
- String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。
- Symbol 类型的值不能转换为数字,会报错。
对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
8. || 和 && 操作符的返回值?
|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。
对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
- && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。
|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果
9. 讲一讲JavaScript的装箱和拆箱?
装箱:把基本数据类型转化为对应的引用数据类型的操作
看以下代码,s1只是一个基本数据类型,他是怎么能调用indexOf的呢?
const s1 = 'Sunshine_Lin'
const index = s1.indexOf('_')
console.log(index) // 8
原来是JavaScript内部进行了装箱操作
- 1、创建String类型的一个实例;
- 2、在实例上调用指定的方法;
- 3、销毁这个实例;
拆箱:将引用数据类型转化为对应的基本数据类型的操作var temp = new String('Sunshine_Lin')
const index = temp.indexOf('_')
temp = null
console.log(index) // 8
通过valueOf或者toString方法实现拆箱操作var objNum = new Number(123);
var objStr = new String("123");
console.log( typeof objNum ); //object
console.log( typeof objStr ); //object
console.log( typeof objNum.valueOf() ); //number
console.log( typeof objStr.valueOf() ); //string
console.log( typeof objNum.toString() ); // string
console.log( typeof objStr.toString() ); // string
10. 请问ES6新增的Symbol数据类型有何特点?
为保证每个属性的名字都是独一无二,从根本上防止属性名冲突,ES6 引入Symbol数据类型
Symbol是第7种基础数据类型,表示独一无二的值,Symbol 值通过Symbol函数生成,对象的属性名现在可以有两种类型,一种是本来的字符串,另一种就是新增的 Symbol 类型
Symbol 数据类型特点:
(1)凡属性名属于 Symbol 类型,就是独一无二的,可以保证不会与其他属性名产生冲突
(2)Symbol数据类型可用 typeof 检测出来,返回“symbol”
(3)Symbol函数前不能使用 new 操作,会报错,因为生成的 Symbol 是一个基础类型的值,不是对象,可理解为它是一种类似于字符串的数据类型
(4)Symbol函数的参数只表示对当前Symbol值的描述,就算参数相同,Symbol函数的返回值是不相等的
(5)在Symbol 作为属性名,遍历对象时,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()遍历返回// 没有参数的情况
let s1 =Symbol();
let s2 =Symbol();
s1 === s2 // false
// 有参数的情况
let s1 =Symbol('foo');
let s2 =Symbol('foo');
s1 === s2 // false
但它并不是私有属性,Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值
Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和 Symbol 键名
常用方法:Symbol.for():(全局注册)
接受一个字符串作为参数,随后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局,可以实现重新使用同一个 Symbol 值。11. new操作符的实现原理
new操作符的执行过程:
(1)首先创建了一个新的空对象
(2)设置原型,将对象的原型设置为函数的 prototype 对象。
(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
具体实现:function mynew(constructor, ...args) {
const obj = {} //等价于const obj = Object.create(constructor.prototype)
obj.__proto__ = constructor.prototype
const res = constructor.apply(obj, args) //拿到返回值
// 如果构造函数没有显式return(通常情况)那么person就是新创建的对象obj
// 如果构造函数返回的不是一个对象,比如1、"abc" 那么person还是新创建的对象obj
// 如果构造函数显式返回了一个对象,比如{}、function() {}
// 那么person就不是新创建的对象obj了,而是显式return的这个对象
return res instanceof Object ? res : obj
}
12. map和Object的区别
|
| Map | Object | | —- | —- | —- | | 意外的键 | Map默认情况不包含任何键,只包含显式插入的键。 | Object 有一个原型, 原型链上的键名有可能和自己在对象上的设置的键名产生冲突。 | | 键的类型 | Map的键可以是任意值,包括函数、对象或任意基本类型。 | Object 的键必须是 String 或是Symbol。 | | 键的顺序 | Map 中的 key 是有序的。因此,当迭代的时候, Map 对象以插入的顺序返回键值。 | Object 的键是无序的 | | Size | Map 的键值对个数可以轻易地通过size 属性获取 | Object 的键值对个数只能手动计算 | | 迭代 | Map 是 iterable 的,所以可以直接被迭代。 | 迭代Object需要以某种方式获取它的键然后才能迭代。 | | 性能 | 在频繁增删键值对的场景下表现更好。 | 在频繁添加和删除键值对的场景下未作出优化。 |
13. map和weakMap的区别
(1)Map map本质上就是键值对的集合,但是普通的Object中的键值对中的键只能是字符串。而ES6提供的Map数据结构类似于对象,但是它的键不限制范围,可以是任意类型,是一种更加完善的Hash结构。如果Map的键是一个原始数据类型,只要两个键严格相同,就视为是同一个键。
实际上Map是一个数组,它的每一个数据也都是一个数组,其形式如下:
const map = [
["name","张三"],
["age",18],
]
Map数据结构有以下操作方法:
- size: map.size 返回Map结构的成员总数。
- set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
- get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
- has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
- delete(key):该方法删除某个键,返回true,如果删除失败,返回false。
- clear():map.clear()清除所有成员,没有返回值。
Map结构原生提供是三个遍历器生成函数和一个遍历方法
- keys():返回键名的遍历器。
- values():返回键值的遍历器。
- entries():返回所有成员的遍历器。
forEach():遍历Map的所有成员。
const map = new Map([
["foo",1],
["bar",2],
])
for(let key of map.keys()){
console.log(key); // foo bar
}
for(let value of map.values()){
console.log(value); // 1 2
}
for(let items of map.entries()){
console.log(items); // ["foo",1] ["bar",2]
}
map.forEach( (value,key,map) => {
console.log(key,value); // foo 1 bar 2
})
(2)WeakMap WeakMap 对象也是一组键值对的集合,其中的键是弱引用的。其键必须是对象,原始数据类型不能作为key值,而值可以是任意的。
该对象也有以下几种方法:set(key,value):设置键名key对应的键值value,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键。(因为返回的是当前Map对象,所以可以链式调用)
- get(key):该方法读取key对应的键值,如果找不到key,返回undefined。
- has(key):该方法返回一个布尔值,表示某个键是否在当前Map对象中。
- delete(key):该方法删除某个键,返回true,如果删除失败,返回false。
其clear()方法已经被弃用,所以可以通过创建一个空的WeakMap并替换原对象来实现清除。
WeakMap的设计目的在于,有时想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。
而WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
总结:
- Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
WeakMap 结构与 Map 结构类似,也是用于生成键值对的集合。但是 WeakMap 只接受对象作为键名( null 除外),不接受其他类型的值作为键名。而且 WeakMap 的键名所指向的对象,不计入垃圾回收机制。
14. 对JSON的理解
JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。
在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为 JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。
因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。
在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,JSON.stringify 函数,通过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,可以调用这个函数将数据对象转化为 JSON 格式的字符串。
JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。
15. JavaScript脚本延迟加载的方式有哪些?
延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。
一般有以下几种方式:defer 属性: 给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
- async 属性: 给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
- 动态创建 DOM 方式: 动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
- 使用 setTimeout 延迟方法: 设置一个定时器来延迟加载js脚本文件
- 让 JS 最后加载: 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。
16. JavaScript 类数组对象的定义?
一个拥有 length 属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。常见的类数组对象有 arguments 和 DOM 方法的返回结果,还有一个函数也可以被看作是类数组对象,因为它含有 length 属性值,代表可接收的参数个数。
常见的类数组转换为数组的方法有这样几种:// (1)通过 call 调用数组的 slice 方法来实现转换
Array.prototype.slice.call(arrayLike);
// (2)通过 call 调用数组的 splice 方法来实现转换
Array.prototype.splice.call(arrayLike, 0);
// (3)通过 apply 调用数组的 concat 方法来实现转换
Array.prototype.concat.apply([], arrayLike);
// (4)通过 Array.from 方法来实现转换
Array.from(arrayLike);
// (5) 使用展开运算符将类数组转化成数组
arrayLike = [...arrayLike]
17. 数组有哪些方法?
| 方法 | 作用 | 是否影响原数组 | | —- | —- | —- | | push | 在数组后添加元素,返回长度 | ✅ | | pop | 删除数组最后一项,返回被删项 | ✅ | | shift | 删除数组第一项,返回被删项 | ✅ | | unshift | 数组开头添加元素,返回长度 | ✅ | | reserve | 反转数组,返回数组 | ✅ | | sort | 排序数组,返回数组 | ✅ | | splice | 截取数组,返回被截取部分splice(start, num, item1, item2, …) | ✅ | | join | 将数组变字符串,返回字符串 | ❌ | | concat | 连接数组 | ❌ | | map | 相同规则处理数组项,返回新数组 | ❌ | | forEach | 遍历数组(没有返回值) | ❌ | | filter | 过滤数组项,返回符合条件的数组 | ❌ | | every | 每一项符合规则才返回true | ❌ | | some | 只要有一项符合规则就返回true | ❌ | | reduce | 接受上一个return和数组下一项 | ❌ | | flat | 数组扁平化 | ❌ | | slice | 截取数组,返回被截取区间slice(start, end) | ❌ |
18. 什么是 DOM 和 BOM?
- DOM 指的是文档对象模型,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口。
- BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的方法和接口。BOM的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。
有哪些常用的 Bom 属性呢?
(1) location 对象
- location.href— 返回或设置当前文档的 URL
- location.search — 返回 URL 中的查询字符串部分。例如 http://www.dreamdu.com/dreamdu.php?id=5&name=dreamdu 返回包括(?)后面的内容?id=5&name=dreamdu
- location.hash — 返回 URL#后面的内容,如果没有#,返回空
- location.host — 返回 URL 中的域名部分,例如 www.dreamdu.com
- location.hostname — 返回 URL 中的主域名部分,例如 dreamdu.com
- location.pathname — 返回 URL 的域名后的部分。例如 http://www.dreamdu.com/xhtml/ 返回/xhtml/
- location.port — 返回 URL 中的端口部分。例如 http://www.dreamdu.com:8080/xhtml/ 返回8080
- location.protocol — 返回 URL 中的协议部分。例如 http://www.dreamdu.com:8080/xhtml/ 返回(//)前面的内容 http:
- location.assign — 设置当前文档的 URL
- location.replace() — 设置当前文档的 URL,并且在 history 对象的地址列表中移除这个 URL location.replace(url)
- location.reload() — 重载当前页面
(2) history 对象
- history.go() — 前进或后退指定的页面数 history.go(num);
- history.back() — 后退一页
- history.forward() — 前进一页
(3) Navigator 对象
- navigator.userAgent — 返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串)
navigator.cookieEnabled — 返回浏览器是否支持(启用)cookie
19. 对AJAX的理解,实现一个AJAX请求
AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
创建AJAX请求的步骤:创建一个 XMLHttpRequest 对象。
- 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
- 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功时
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);
使用Promise封装AJAX:
// promise 封装实现:
function getJSON(url) {
// 创建一个 promise 对象
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
// 新建一个 http 请求
xhr.open("GET", url, true);
// 设置状态的监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功或失败时,改变 promise 的状态
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
// 设置错误监听函数
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 设置响应的数据类型
xhr.responseType = "json";
// 设置请求头信息
xhr.setRequestHeader("Accept", "application/json");
// 发送 http 请求
xhr.send(null);
});
return promise;
}
20. JavaScript为什么要进行变量提升,它导致了什么问题?
变量提升的表现是,无论在函数中何处位置声明的变量,好像都被提升到了函数的首部,可以在变量声明前访问到而不会报错。
造成变量声明提升的本质原因是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。
首先要知道,JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。在解析阶段JS会检查语法,并对函数进行预编译。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。
- 全局上下文:变量定义,函数声明
- 函数上下文:变量定义,函数声明,this,arguments
- 在执行阶段,就是按照代码的顺序依次执行。
那为什么会进行变量提升呢?主要有以下两个原因:
- 提高性能
- 容错性更好
(1)提高性能 在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),而这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。
在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。
(2)容错性更好
变量提升可以在一定程度上提高JS的容错性,看下面的代码:
a = 1;
var a;
console.log(a);
如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。
虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。
总结:
- 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
- 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行
变量提升虽然有一些优点,但是他也会造成一定的问题,在ES6中提出了let、const来定义变量,它们有变量提升的机制但是存在暂时性死区。下面看一下变量提升可能会导致的问题:
var tmp = new Date();
function fn(){
console.log(tmp);
if(false){
var tmp = 'hello world';
}
}
fn(); // undefined
在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。
var tmp = 'hello world';
for (var i = 0; i < tmp.length; i++) {
console.log(tmp[i]);
}
console.log(i); // 11
由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以打印出来11。
21. 什么是尾调用,使用尾调用有什么好处?
尾调用指的是函数的最后一步调用另一个函数。代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
22. 请问你了解js模块化吗?
js模块化所解决问题:
命名冲突:一些变量和函数命名可能相同
文件依赖:一些需要从外部引入的文件数目、顺序
js模块化将按照功能将一个软件切分成许多单独部分,每个部分为一个模块,然后再组装起来。分模块进行使用与维护,提高开发效率。
js模块化发展过程:
(1)script标签
最早期的js文件加载方式,把每个文件看做一个模块,接口通常直接暴露在全局作用域(定义在window对象中)
缺点:加载顺序取决于script标签书写顺序
易污染全局作用域
各文件间的依赖关系较繁琐
(2)CommonJS
每个文件就是一个模块,有自己的作用域,在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载,在浏览器端,模块需要提前编译打包处理。
//暴露模块:
module.exports = value 或 exports.xxx = value
//引入模块:
require(xxx)
//如果是第三方模块,xxx为模块名;如果是自定义模块,xxx为模块文件路径
// module add.js
module.exports = function add (a, b) { return a + b; }
// main.js
var {add} = require('./math');
console.log('1 + 2 = ' + add(1,2);
CommonJS加载模块是同步的,只有加载完成,才能执行后面的操作,造成一个重大的局限:不适用于浏览器
同步加载对服务器端影响不大,可把所有的模块都存在本地硬盘,同步加载,等待时间就是读取硬盘时间。但对于浏览器,因为模块都放在服务器端,等待时间取决于网速的快慢,长时间等待会造成浏览器处于”假死”状态
浏览器端的模块不能采用同步加载,只能采用异步加载,便有了AMD。
(3)AMD
非同步加载模块,允许指定回调函数,浏览器端一般采用AMD
优点:
(1)适合在浏览器环境中异步加载模块 (2)可以并行加载多个模块
//定义没有依赖的模块
define(function(){
return 模块
})
//定义有依赖的模块
define(['module1', 'module2'], function(m1, m2){
return 模块
})
//引入使用模块
require(['module1', 'module2'], function(m1, m2){
//使用m1/m2
})
(4)CMD
专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行(延迟执行)
//定义没有依赖的模块
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
//引入依赖模块(同步)
var module2 = require('./module2')
//引入依赖模块(异步)
require.async('./module3', function (m3) {
})
//暴露模块
exports.xxx = value
})
//引入使用模块
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
CMD与AMD区别:
最大的区别是对依赖模块的执行时机处理不同,二者皆为异步加载模块
AMD依赖前置,js可以方便知道依赖模块是谁,立即加载
CMD就近依赖,需要使用把模块变为字符串解析一遍才知道依赖了那些模块,延迟执行
(4)UMD
严格上说,UMD不能算是一种模块规范,它主要用来处理CommonJS、AMD、CMD的差异兼容,使模块代码能在不同的模块环境下都能正常运行,是模块定义的跨平台解决方案
(5)ES6模块化
设计思想:尽可能静态化,使得编译时就能确定模块的依赖关系、输入和输出变量,CommonJS 和 AMD 模块,都只能在运行时确定
//导出模块方式
var a = 0;
export { a }; //第一种
export const b = 1; //第二种
let c = 2;
export default { c }//第三种
let d = 2;
export default { d as e }//第四种,别名
//导入模块方式
import { a } from './a.js' //针对export导出方式,.js后缀可省略
import main from './c' //针对export default导出方式,使用时用 main.c
import 'lodash' //仅仅执行lodash模块,但是不输入任何值
主要由export和import两个命令构成,export用于规定模块的对外接口,import用于输入其他模块提供的功能
总结与对比:
CommonJS:主要用于服务端,同步加载模块,并不适合在浏览器环境
AMD:在浏览器中异步加载模块,且可并行加载多个模块,但开发成本相对高,代码阅读和书写较困难,模块定义方式语义不顺畅
CMD:与AMD相似,都用于浏览器,依赖就近,延迟执行,很容易在Node.js中运行
ES6模块化:异步加载,有一个独立的模块依赖的解析阶段,实现相对简单,浏览器和服务器通用模块解决方案
23. 常见的DOM操作有哪些
1)DOM 节点的获取
DOM 节点的获取的API及使用:
// 按照 id 查询
var imooc = document.getElementById('imooc') // 查询到 id 为 imooc 的元素
// 按照标签名查询
var pList = document.getElementsByTagName('p') // 查询到标签为 p 的集合
console.log(divList.length)
console.log(divList[0])
// 按照类名查询
var moocList = document.getElementsByClassName('mooc') // 查询到类名为 mooc 的集合
// 按照 css 选择器查询
var pList = document.querySelectorAll('.mooc') // 查询到类名为 mooc 的集合
2)DOM 节点的创建
// 首先获取父节点
var container = document.getElementById('container')
// 创建新节点
var targetSpan = document.createElement('span')
// 设置 span 节点的内容
targetSpan.innerHTML = 'hello world'
// 把新创建的元素塞进父节点里去
container.appendChild(targetSpan)
3)DOM 节点的删除
// 删除目标元素
container.removeChild(targetNode)
4)修改 DOM 元素
修改 DOM 元素这个动作可以分很多维度,比如说移动 DOM 元素的位置,修改 DOM 元素的属性等。
现在需要调换 title 和 content 的位置,可以考虑 insertBefore 或者 appendChild:
// 获取父元素
var container = document.getElementById('container')
// 获取两个需要被交换的元素
var title = document.getElementById('title')
var content = document.getElementById('content')
// 交换两个元素,把 content 置于 title 前面
container.insertBefore(content, title)
24. 数组的遍历方法有哪些
方法 | 是否改变原数组 | 特点 |
---|---|---|
forEach() | 否 | 数组方法,不改变原数组,没有返回值 |
map() | 否 | 数组方法,不改变原数组,有返回值,可链式调用 |
filter() | 否 | 数组方法,过滤数组,返回包含符合条件的元素的数组,可链式调用 |
for…of | 否 | for…of遍历具有Iterator迭代器的对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象,将异步循环变成同步循环 |
every() 和 some() | 否 | 数组方法,some()只要有一个是true,便返回true;而every()只要有一个是false,便返回false. |
find() 和 findIndex() | 否 | 数组方法,find()返回的是第一个符合条件的值;findIndex()返回的是第一个返回条件的值的索引值 |
reduce() 和 reduceRight() | 否 | 数组方法,reduce()对数组正序操作;reduceRight()对数组逆序操作 |
- for循环
用临时变量将长度缓存起来,避免重复获取数组长度,当数组较大时优化效果比较明显,写法比较繁琐
2. forEach
遍历数组中的每一项,没有返回值,即使有return,也不会返回任何值,执行速度比map()快//参数:item数组中的当前索引的值, index当前项的索引, array原始数组
arr.forEach((item,index,array)=>{
})
- map
创建一个新的数组,新数组的每一个元素由调用数组中的每一个元素执行提供的函数得来,有return返回值
return的意义:不影响原来的数组,只是把原数组克隆一份,改变克隆的数组中的对应项
4. for of
遍历value,适用遍历数组对象、字符串、map、set等拥有迭代器对象的集合,不能遍历对象,因为没有迭代器
与forEach()区别:可以正确响应break、continue和return语句
与for in 区别:无法循环遍历对象,不会遍历自定义属性
5. reduce()
接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,得出最终计算值
相当于:为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素arr.reduce(function(total, currentValue, currentIndex, arr), initialValue)
简单用法:数组求和,求乘积
var arr =[1,2,3,4]
var sum = arr.reduce((x,y)=>x+y)
var mul = arr.reduce((x,y)=>x*y)
console.log(sum);//求和,10
console.log(mul);//求乘积,24
复杂用法:
计算数组中每个元素出现的次数
let names =['Alice','Bob','Tiff','Bruce','Alice'];
let nameNum = names.reduce((pre,cur)=>{
if(cur in pre){
pre[cur]++}
else{
pre[cur]=1}
return pre
},{})
console.log(nameNum);//{Alice: 2, Bob: 1, Tiff: 1, Bruce: 1}
数组去重
let arr =[1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{
if(!pre.includes(cur)){
return pre.concat(cur)
}else{
return pre
}
},[])
console.log(newArr);// [1, 2, 3, 4]
将多维数组转化为一维(又名数组扁平化, 面试高频代码题)
let arr =[[0,1],[2,3],[4,[5,6,7]]]
const newArr=function(arr){
return arr.reduce((pre,cur)=> pre.concat(Array.isArray(cur) ? newArr(cur):cur),[])
}
console.log(newArr(arr));//[0, 1, 2, 3, 4, 5, 6, 7]
25. 请问js有哪些继承方式
js常用继承方式主要有6种:原型链继承、构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承
创造一个超类型的构造函数Super(),为它设置静态属性name、原型链方法getSuper()
function Super(){
this.name =["super"];
}
Super.prototype.getSuper = function(){
return this.name;
}
再创造一个子类构造函数Sub(),使用以上6种继承方法让Sub()继承Super()
function Sub(){}
- 原型链继承(将子类的原型对象指向超类型的实例) ```javascript Sub.prototype = new Super(); //将Sub的原型对象Sub.prototype指向Super的实例
var sub1 =new Sub(); //创建Sub的实例sub1 sub1.name.push(“sub1”);
var sub2 =new Sub(); //创建Sub的实例sub2 sub2.name.push(“sub2”);
console.log(sub2.getSuper())//[“super”, “sub1”, “sub2”]
这样可以在Sub中继承 Super的属性name以及原型链方法getSuper,然而在sub1中修改name时,sub2的name也会受到影响<br />这种继承方式的缺点是:<br />(1)所有实例共享父类中的属性和方法(如果new父类时传参,则属性也都是一样的)。<br />(2)子类的实例不能向父类型构造函数传参
- **构造函数继承(子类中使用call调用超类)**
```javascript
function Sub(name){
Super.call(this, name); //在Sub中使用call去调用Super
}
var sub1 = new Sub("Tom");
console.log(sub1.getSuper()) //Uncaught TypeError(不能继承原型链方法)
console.log(sub1.name)//Tom
var sub2 =new Sub();
console.log(sub2.name) //undefined
var sup =new Super()
console.log(sup.getSuper())//undefined
在Sub中用call调用Super,继承了Super的所有静态属性。在实例sub1、sub2中,各自对name的修改也互不影响,实现了属性不共享,子类的实例也能向超类型构造函数传参
这种继承方式的缺点是:
(1)不能继承原型链方法
- 组合继承(原型链继承+构造函数继承)函数式继承 ```javascript function Sub(name){ Super.call(this, name); //第二次调用父类构造函数,构造函数继承 } Sub.prototype = new Super(); //第一次调用,原型链继承 Sub.prototype.constructor = Sub;
var sub1 =new Sub(“Tom”); console.log(sub1.getSuper()) //Tom console.log(sub1.name) //Tom console.log(sub1 instanceof Sub) //true console.log(sub1 instanceof Super) //true
var sub2 =new Sub(); console.log(sub2.name) //undefined
在子类Sub中,使用 call继承超类型的属性 + 原型链继承原型链的方法和属性,弥补了上面两种继承方式的三个缺点<br />这种继承方式的缺点是:<br />(1)调用了两次父类的构造函数<br />第一次:Sub.prototype = new Super(),调用一次超类型构造函数<br />第二次:Sub内使用call方法,又调用了一次超类型构造函数,且之后每次实例化子类sub1、sub2...的过程中( new Sub() ),都会调用超类型构造函数
- **原型式继承**(创造了一个临时的构造函数F,将 F的原型指向传进来的对象参数,再返回F的实例)
```javascript
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var person ={
name:"Nicholas",
friends:["Sherlly","Van"],
getname:function() {
return this.name;
}
}
var people1 = object(person);
// var people1 = Object.create(person);在传入一个参数的情况下,Object.create()和object()相同
people1.name ="Greg";
people1.friends.push("Rob");
var people2 = object(person);
people2.name ="Linda";
people2.friends.push("Barbie");
console.log(person.name);//Nicholas
console.log(person.friends);//["Sherlly", "Van", "Rob", "Barbie"]
原型式继承和原型链继承类似,区别:前者是完成了一次对对象的浅拷贝,后者是对构造函数进行继承。
注意:ES5的Object.create()在只有一个参数时与这里的object方法是一样的。Object.create()接受两个参数:作为新对象原型的对象,以及给新对象定义额外属性的对象(可选)。第二个参数与Object.defineProperties()的第二个参数一样:每个新增属性都通过各自的描述符来描述。以这种方式添加的属性会遮蔽原型对象上的同名属性。
缺点也是一致的:属性会被共享
寄生式继承(基于原型式继承的封装)
//原型式继承
function object(o){
function F(){}
F.prototype = o;
return new F();
}
//寄生式继承
function createAnother(o){
let clone = object(o)
clone.sayHi = function(){
console.log("hi")
}
return clone;
}
var person ={
name:"Nicholas",
friends:["Sherlly","Van"]
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi()
此方法使用较少,本质上可以通过寄生式继承实现子类方法sayHi的复用,后面通过createAnother()创造出来的对象,都拥有sayHi方法
寄生组合式继承
在组合继承中,若需要优化一次调用,那一定是第一次调用:原型链继承,利用原型式继承便可实现
Sub.prototype = new Super(),实质上就是一次对超类型原型对象的拷贝
function inheritPrototype(subType, superType){
//复制超类型的原型对象:
//将构造函数指向子类型:
subType.prototype = Object.create(superType.prototype);
subType.prototype.constructor = subType;
}
function Super(name){
this.name = name
}
function Sub(name){
Super.call(this, name);//第二次调用
}
Super.prototype.getSuper = function() {
return this.name;
}
inheritPrototype(Sub, Super);
var sub1 = new Sub("Tom");
console.log(sub1.getSuper()) //Tom
console.log(sub1.name) //Tom
console.log(sub1 instanceof Sub) //true
console.log(sub1 instanceof Super) //true
var sub2 =new Sub();
console.log(sub2.name) //undefined
子类对超类型的原型对象的继承,分为以下几个步骤:
(1)封装一个 inheritPrototype 函数
(2)利用object(或Object.create())复制出超类型的原型对象
(3)将原型对象的构造函数指向自身(把名字改成自己的:clone.constructor = subType,constructor相当于一张身份证,身份证上的名字一定得是自己)
(4)将拷贝出来的对象传递给子类的原型对象
结合性记忆:原型链继承+构造函数继承 = 组合继承;为了优化组合继承→原型式继承→寄生式继承→寄生组合式继承
- ES6继承 ```javascript // 定义一个父类 class Person { constructor() { this.type = ‘person’ } } // 定义一个子类 class Student extends Person { constructor() { super() } }
let student = new Student() student.type // ‘person’
<a name="Isp44"></a>
### 26. 请问什么是浅拷贝?什么是深拷贝
浅拷贝只拷贝对象的第一层属性,如果是引用数据类型只拷贝存在栈里的指针。<br />在js中, 分基本数据类型与引用数据类型,这两类数据存储分别是:<br />基本数据类型:名与值都存储在栈内存中,例如let a=1:<br /><br />当b=a时,b复制了a的值,栈内存会新开辟一个内存给b,<br /><br />当修改a=2时,对b并不会造成影响,因为此时的b具有独立的存储空间,不受a的影响了。<br /><br />引用数据类型:名存在栈内存中,值存在于堆内存中,栈内存会提供一个引用的地址指向堆内存中的值。<br /><br />当b=a时,其实b复制了a的引用地址,而并非堆内存中的值,而当a[0]=1时(整个数组的值在堆中)进行数组修改时,由于a与b指向的是同一个地址,自然b也受影响,这就是所谓的浅拷贝。<br /><br />**实现深拷贝:**
1. 递归复制所有层级属性(面试高频撕代码题)
可理解为一层层地复制对象中的属性, 直到值为基础类型,缺点:代码较为复杂
```javascript
//使用递归的方式实现数组、对象的深拷贝
function deepClone(obj) {
//判断拷贝的要进行深拷贝的是数组还是对象,是数组的话进行数组拷贝,对象的话进行对象拷贝
var objClone = Array.isArray(obj) ? [] : {};
//进行深拷贝的不能为空,并且是对象
if (obj && typeof obj === "object") {
for (let key in obj) { //for in会遍历原型对象上的属性
if (obj.hasOwnProperty(key)) {
if (obj[key] && typeof obj[key] === "object") {
objClone[key] = deepClone(obj[key]);
} else {
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
- 借助JSON对象的parse和stringify
利用js的内置对象JSON来进行数组对象的深拷贝,缺点:无法实现对象中方法的深拷贝
function deepClone(obj) {
var _obj = JSON.stringify(obj);
objClone = JSON.parse(_obj);
return objClone;
}
- Object.assign()拷贝
当对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝
- lodash函数库
lodash是一个很热门的函数库,可利用lodash.cloneDeep()实现深拷贝
27. 防抖与节流
函数防抖(debounce):触发高频事件后n秒内,函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。1、电脑息屏时间,每动一次电脑又重新计算时间。2、input框变化频繁触发事件可加防抖。3、频繁点击按钮提交表单可加防抖
函数节流(throttle):高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率。1、滚动频繁请求列表可加节流。2、游戏里长按鼠标,但是动作都是每隔一段时间做一次
两者都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象
注意:防抖函数的代码可以获取 this 和 参数,是为了让 debounce 函数最终返回的函数 this 指向不变以及依旧能接受到 e 参数。
let telInput = document.querySelector('input');
//!!!让debounce返回一个函数,然后input事件触发这个函数,args是e事件对象
telInput.addEventListener('input',debounce(demo, 2000))
//封装防抖
function debounce(fn, wait) {
let timeOut = null;
return args => {
if(timeOut) clearTimeout(timeOut); //有就清除,然后重新创建一个定时器
timeOut = setTimeout(fn, wait)
}
}
function demo() {
console.log('发起请求');
}
节流:(每隔一段时间发一次 Ajax 请求,用节流)
规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效
实现思路:通过判断是否到达一定时间来触发函数,若没到规定时间则使用计时器延后,而下一次事件则会重新设定计时器
function throttle(fn,delay) {
let canRun = true; // 通过闭包保存一个标记
return function () {
// 在函数开头判断标记是否为true,不为true则return
if (!canRun) return;
// 立即设置为false
canRun = false;
// 将外部传入的函数的执行放在setTimeout中
setTimeout(() => {
// 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。
// 当定时器没有执行的时候标记永远是false,在开头被return掉
fn.apply(this, arguments);
canRun = true;
}, delay);
};
}
let box = document.querySelector('.box');
box.addEventListener('touchmove',throttle(demo,2000))
function throttle(event, time) {
let timer = null;
return function() {
if(!timer) {
timer = setTimeout(() => {
event();
timer = null;
},time)
}
}
}
28. 请问js有哪些数组去重方法?
for 循环(一次)+ indexOf() + 新数组
function sort(arr) {
var result = new Array();
for(var i = 0; i < arr.length; i++) {
if(result.indexOf(arr[i]) == -1)
result.push(arr[i]);
}
return result;
}
sort()
function sort(arr) {
arr = arr.sort()
var result= [arr[0]];
for (var i = 1; i < arr.length; i++) {
if (arr[i] !== arr[i-1]) {
result.push(arr[i]);
}
}
return result;
}
Map
function sort(arr) {
let map = new Map();
let result = new Array();
for (let i = 0; i < arr.length; i++) {
if(map.has(arr[i])) { // 如果有该key值
map.set(arr[i], true);
} else {
map.set(arr[i], false); // 如果没有该key值
result.push(arr[i]);
}
}
return result ;
}
new Set() + …(展开运算符)
function sort(arr) {
return [...new Set(arr)];
}
for循环(一次) + 新对象
function sort(arr) {
let obj = {};
for (let i = 0; i < arr.length; i++) {
if (!obj[arr[i]]) {
obj[arr[i]] = 1;
}
}
return Object.keys(obj);;
}
29. 请问你了解js事件循环机制(Event Loop)吗
js是一门主要运行在浏览器的脚本语言,主要用途之一是操作DOM元素
若js同时有两个线程,对同一个DOM元素进行操作,这时浏览器应该听哪个线程的?如何判断优先级?为了避免这种问题,js必须是一门单线程语言。
主线程:即主线程会不停的从执行栈中读取事件,直至执行完所有栈中的同步代码
任务队列:当遇到一个异步事件后,js并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,即任务队列
异步任务:分为 宏任务(macrotask) 与 微任务 (microtask),不同的API注册的任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行
常见宏任务(macrotask):
script(整体代码)、setTimeout、setInterval、UI 渲染、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 环境)
常见微任务(microtask):
Promise、 MutaionObserver、process.nextTick(Node.js环境)
Event Loop(事件循环):宏任务 > 所有微任务 > 宏任务(主要针对V8)
- 执行栈选择最先进入队列的宏任务(通常是script整体代码),如果有则执行
- 检查是否存在 Microtask,如果存在则不停地执行,直至清空 microtask 队列
- 更新render(每一次事件循环,浏览器都可能会去更新渲染)
- 重复以上步骤
一起看两道经典面试题:(主要考察执行顺序问题,考察频率极高)
//题目一
setTimeout(function () {
console.log(1);
});
new Promise(function(resolve,reject){
console.log(2)
resolve()
}).then(function(){
console.log(3);
})
console.log(4);
先执行script同步代码:先执行Promise中的console.log(2),再执行console.log(4)
再执行微任务:Promise的then函数
最后执行定时器中的console.log(1),最终输出顺序为:2,4,3,1
注意:对于Promise,本身是同步的, Promise.then是异步的
//题目二(稍微复杂一点,建议大家结合分析多看几遍)
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
首先执行同步代码:
(1)执行 console.log(‘script start’)
(2)执行 async1() ,马上执行 async2函数:console.log(‘async2 end’)
(3)执行 new Promise()中的同步函数:console.log(‘Promise’)
(4)最后执行 console.log(‘script end’),同步代码执行完毕
看剩下的异步代码:
(5)setTimeout是宏任务,留到最后
剩下微任务:
async function async1() {
await async2()
console.log('async1 end')
}
new Promise(resolve => {
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
(6)根据队列的先入先出方式,先执行 await async2() 后面的函数 console.log(‘async1 end’)
(7)执行promise的resolve函数
new Promise(resolve => { resolve() })
也就是两个then: console.log(‘promise1’) 、console.log(‘promise2’)
(8)最后执行宏任务 setTimeout函数 console.log(‘setTimeout’)
综上所述,以上代码执行的顺序是:
“script start”、“async2 end”、“Promise”、“script end”、“async1 end”、“promise1”、 “promise2 ”、“setTimeout”
30. 请问什么是事件流?
js与html页面的交互是通过DOM事件实现的,事件流 指页面接收事件的顺序
DOM事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段
利用简单的HTML页面为例,进行具体分析:
事件冒泡:即事件开始时,由最具体的元素(也就是事件发生所在的节点)接收,然后逐级向上传播到较为不具体的节点(文档)(摘自《JavaScript高级程序设计》)
对于上述页面中, 当单击