【JavaScript】Interview必背(详细版)
- 本文进行了相关文章整理总结和修改。
文章目录
- 1.介绍一下js的数据类型有哪些,值是如何存储的
- 2. && 、 || 和 !! 运算符分别能做什么
- 3.数据类型转换
- 4.数据类型判断 typeof,instanceof,constructor,Object.prototype.toString.call()
- 5. 介绍 js 有哪些内置对象?
- 6.undefined与undeclared的区别
- 7.null和undefined的区别
- 8.{}和[]的valueOf和toString的结果是什么
- 9.JS的作用域和作用域链
- 10.JS创建对象的几种方式
- 11.JS继承的几种实现方式
- 12.call和apply的用法(详细介绍)
- 13.原型与原型链
- 14.什么是闭包,为什么要用它?
- 15.了解DOM和BOM吗?
- 16.三种事件模型是什么?
- 17.事件委托是什么?
- 18.事件传播是什么?
- 19.事件捕获是什么?
- 20.事件冒泡是什么?
- 21.DOM操作 添加移除移动复制创建和查找结点
- 22.js数组和字符串原生方法
- 23.常用正则表达式
- 24.Ajax 是什么? 如何创建一个 Ajax?
- 25. js 延迟加载的方式有哪些?
- 26.谈谈你对模块化开发的理解?
- 27.js 的几种模块规范?模块加载方案?
- 28.AMD 和 CMD 规范的区别?
- 29.ES6 模块与 CommonJS 模块、AMD、CMD 的差异。
- 30. requireJS的核心原理是什么?
- 31.谈谈JS的运行机制
- 32.arguments 的对象是什么?
- 33.为什么在调用这个函数时,代码中的b会变成一个全局变量?
- 34.简单介绍一下V8引擎的垃圾回收机制
- 35.哪些操作会造成内存泄漏
- 36.ECMAScript是什么?
- 37.ES6有哪些新特性?
- 38.var let const 区别
- 39.什么是箭头函数?
- 40.什么是类?
- 41.什么是模板字符串?
- 42.什么是对象解构?
- 43.什么是Set对象,它是如何工作的?
- 44.什么是Proxy?
- 45.写一个通用的事件侦听器函数
- 46.什么是函数式编程?JS有哪些特性使其成为函数式语言
- 47.什么是高阶函数?
- 48.为什么函数被称为一等公民?
- 49.手动实现Array.prototype.map()方法
- 50.手动实现Array.prototype.filter()方法
- 51.手动实现Array.prototype.reduce()方法
- 52.JS的深浅拷贝
- 53.手写call apply bind
- 54.函数柯里化的实现
- 55.js模拟new操作符的实现
- 56.什么是回调函数?回调函数有什么缺点?
- 57.Promise是什么,可以手写实现一下吗?
- 58.Iterator是什么,有什么作用?
- 59.Generator函数是什么,有什么作用?
- 60.什么是 async/await 及其如何工作,有什么优缺点?
- 61.instanceof的原理是什么,如何实现?
- 62.js 的节流与防抖
1.介绍一下js的数据类型有哪些,值是如何存储的
- JavaScript一共有8种数据类型,其中有7种基本数据类型:
Undefined、Null、Boolean、Number、String、Symbol(es6新增,表示独一无二的值)和BigInt(es10新增)
;
1种引用数据类型——Object(Object本质上是由一组无序的名值对组成的)。里面包含 Dunction、Array、Date等。
JavaScript不支持任何创建自定义类型的机制,而所有值最终都将是上述 8 种数据类型之一。 - 原始数据类型:直接存储在栈(stack)中,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。
- 引用数据类型:同时存储在栈(stack)和堆(heap)中,占据空间大、大小不固定。引用数据类型在
栈中存储了指针,该指针指向堆中该实体的起始地址
。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
2. && 、 || 和 !! 运算符分别能做什么
- && 叫逻辑与,在其操作数中找到第一个虚值表达式并返回它,如果没有找到任何虚值表达式,则返回最后一个真值表达式。它采用短路来防止不必要的工作。
- || 叫逻辑或,在其操作数中找到第一个真值表达式并返回它。这也使用了短路来防止不必要的工作。在支持 ES6 默认函数参数之前,它用于初始化函数中的默认参数值。
- !! 运算符可以将右侧的值强制转换为布尔值,这也是将值转换为布尔值的一种简单方法。、、
3.数据类型转换
只有三种情况,分别是
- 转换为布尔值(Boolean())
- 转换为数字(Number()、parseInt()、parseFloat())
- 转换为字符串(.toString()或者String()方法)
null和underfined没有.toString方法
4.数据类型判断 typeof,instanceof,constructor,Object.prototype.toString.call()
- typeof
typeof 对于原始类型来说,除了 null 都可以显示正确的类型
被解释为object类型的还有数组和null
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object []数组的数据类型在 typeof 中被解释为 object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object null 的数据类型被 typeof 解释为 object
typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型,所以想判断一个对象的正确类型,这时候可以考虑使用 instanceof
- instanceof
instanceof可以正确判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的prototype
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
// console.log(undefined instanceof Undefined);
// console.log(null instanceof Null);
可以看出instanceof可以精确判断出一个引用数据类型的类型,但是不能精确判断出基本数据类型
因为instanceof是用来测试一个对象在该原型链中是否存在构造函数的prototype属性,也就是判断该对象是否是某一数据类型的实例。
而数字类型,字符串类型,布尔类型不是实例,所以为false。
- constructor
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
这也是通过判断构造函数中的prototype来进行的
但是修改prototype过后就会使constructor返回值修改,即使他的原型是本来的,但是原型的prototype改了。
function Fn(){};
Fn.prototype=new Array();
var f=new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
- Object.prototype.toString.call()
var a = Object.prototype.toString;
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));
用call()对传进去的值进行该值的原型链prototype的构造函数type转换为字符串过后的回调
5. 介绍 js 有哪些内置对象?
可以理解成有哪些内核定义的对象
js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般我们经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。
全局的对象( global objects )或称标准内置对象,不要和 “全局对象(global object)” 混淆。这里说的全局的对象是说在
全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。
标准内置对象的分类
(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。
例如 Infinity、NaN、undefined、null 字面量
(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。
例如 eval()、parseFloat()、parseInt() 等
(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。
例如 Object、Function、Boolean、Symbol、Error 等
(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。
例如 Number、Math、Date
(5)字符串,用来表示和操作字符串的对象。
例如 String、RegExp
(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array
(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。
例如 Map、Set、WeakMap、WeakSet
(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。
例如 SIMD 等
(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。
例如 JSON 等
(10)控制抽象对象
例如 Promise、Generator 等
(11)反射
例如 Reflect、Proxy
(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。
例如 Intl、Intl.Collator 等
(13)WebAssembly
(14)其他
例如 arguments
6.undefined与undeclared的区别
- 已在作用域声明但还没有赋值的变量,是undefined。相反,还没有在作用域中声明的变量是undeclared。
- undeclared的变量引用浏览器会报错。但是我们可以用typeof(…)避免,因为返回的是undefined
7.null和undefined的区别
- 都是基本数据类型,都只有一个值
- undefined是未定义,null代表空对象,主要进行可能会返回对象的变量赋值初始化。
- typeof判断NULL时候返回的是“object”
- 双等号对两个类型的值进行比较会返回true,但是三等号会返回false
8.{}和[]的valueOf和toString的结果是什么
- {} 的 valueOf 结果为 {} ,toString 的结果为 “[object Object]”
- [] 的 valueOf 结果为 [] ,toString 的结果为 “”
9.JS的作用域和作用域链
- 作用域:作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找
- 作用域链:作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和函数。
作用域链本质是指向变量对象的指针列表。
作用域链的前端始终都是当前执行上下文的变量对象,全局对象始终都是作用域链的最后一个对象
查找变量的时候,如果当前没找到,可以沿着作用域链往后查找
创建过程与执行上下文的建立有关
10.JS创建对象的几种方式
- Object构造函数创建
var Person = new Object();
Person.name = 'Nike';
Person.age = 29;
Person指向用new创建Object引用类型的一个新实例,然后进行值的添加
- 使用对象字面量表示
var Person = {};//相当于var Person = new Object();
Person = {
name:'Nike';
age:29;
}
- 工厂模式:
用函数封装对象细节,通过调用函数进行复用。创建出来的对象无法和某个类型联系起来,没有建立对象和类型间的关系。
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson('Nike',29,'teacher');
var person2 = createPerson('Arvin',20,'student');
- 构造函数模式:
只要一个函数通过new调用,就可以称为构造函数。
首先会创建一个对象,然后将对象的原型指向构造函数的prototype,将执行上下文的this值向这个对象,然后用this给对象赋值。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person('Nike',29,'teacher');
var person2 = new Person('Arvin',20,'student');
优点是:
1.创建的对象和构造函数建立了关系,就是prototype,因此可以通过原型来识别对象的类型;也就是说可以通过该对象的原型的prototype判断类型:
2.无显示创建对象
3.直接将属性和方法赋值给了this对象
4.无return
alert(person1 instanceof Object);//ture
alert(person1 instanceof Person);//ture
//instanceof还可以换成
//1.person1.constructor === Object;
//var a = Object.prototype.toString; a.call(person1) === Person
缺点是:每个方法都要在每个实例上重新创建一遍,方法指的就是我们在对象里面定义的函数。如果方法的数量很多,就会占用很多不必要的内存。
- 原型创建对象模式:
function Person(){}
Person.prototype.name = 'Nike';
Person.prototype.age = 20;
Person.prototype.jbo = 'teacher';
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name ='Greg';
alert(person1.name); //'Greg' --来自实例
alert(person2.name); //'Nike' --来自原型
- 组合使用构造函数模式和原型模式
function Person(name,age,job){
this.name =name;
this.age = age;
this.job = job;
}
Person.prototype = {
constructor:Person,
sayName: function(){
alert(this.name);
};
}
var person1 = new Person('Nike',20,'teacher');
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性
11.JS继承的几种实现方式
由于JS并不是真正面向对象的语言,是基于对象的,没有类的概念,所以继承就要用JS对象的原型prototype机制或者用apply和call方法实现。
可以把prototype看作一个模板,新创建的对象都是这个模板的拷贝(新实例化的对象内部有个_Proto_指针,指向原型对象)
- 继承的第一种方式:对象冒充
通过this.某某函数()将parent函数引入,然后对其进行传参操作,此时该子对象this已经有parent的值了,也就是说一个传参后的副本,销毁最开始的parent引入,就只剩this自己的继承属性了
function Parent(username){
this.username = username;
this.hello = function(){
console.log('hello ' + this.username);
}
}
Parent.prototype.sayMorning = function(){
console.log('good morning ' + this.username);
}
function Child(username,password){
//通过以下3行实现将Parent的属性和方法追加到Child中,从而实现继承
//第一步:this.method是作为一个临时的属性,并且指向Parent所指向的对象,
//第二步:执行this.method方法,即执行Parent所指向的对象函数
//第三步:销毁this.method属性,即此时Child就已经拥有了Parent的所有属性和方法
this.method = Parent;
this.method(username);//最关键的一行
delete this.method;
this.password = password;
this.world = function(){
console.log(this.password);
}
}
var parent = new Parent("zhangsan");
var child = new Child("lisi","123456");
parent.hello();
parent.sayMorning();
child.hello();
child.world();
- 继承的第二种方式:call()方法方式
因为call函数能够回调全局对象的函数/全局函数将该函数赋值给传进去的this上,所以call返回的this对象就是进行父函数回调处理过后的对象,就能够实现继承。
但是this的原型还是child的原型!!!因为传进去的this本身就是child
function Parent(username){
this.username = username;
this.hello = function(){
console.log(this.username);
}
}
Parent.prototype.sayMorning = function(){
console.log('good morning ' + this.username);
}
function Child(username,password){
Parent.call(this,username);
this.password = password;
this.world = function(){
console.log(this.password);
}
}
var parent = new Parent("zhangsan");
var child = new Child("lisi","123456");
parent.hello();
parent.sayMorning();
child.hello();
child.world();
// child.sayMorning(); 通过prototype 添加的方法和属性,不能用来继承
- 继承的第三种方式:apply()方法方式
原理跟call一样,只是传进去的值第一个是this,第二个必须是有内存分配(不是野指针)的数组;例如new Array()或者 var []
function Parent(username){
this.username = username;
this.hello = function(){
console.log(this.username);
}
}
Parent.prototype.sayMorning = function(){
console.log('good morning ' + this.username);
}
function Child(username,password){
Parent.apply(this,new Array(username));
this.password = password;
this.world = function(){
console.log(this.password);
}
}
var parent = new Parent("zhangsan");
var child = new Child("lisi","123456");
parent.hello();
parent.sayMorning();
child.hello();
child.world();
// child.sayMorning(); 通过prototype 添加的方法和属性,不能用来继承
- 继承的第四种方式:原型链方式
原型链方式,即子类通过prototype将所有在父类中通过prototype追加的属性和方法都追加到child里面,从而实现继承。
function Person(){
}
Person.prototype.hello = "hello";
Person.prototype.sayHello = function(){
console.log(this.hello);
}
function Child(){
}
Child.prototype = new Person();//这行的作用是:将Parent中将所有通过prototype追加的属性和方法都追加到Child,从而实现了继承
console.log("判断Child的原型链是不是Person原型的一个实例化对象 " + (Child.prototype.__proto__ === Person.prototype))
//判断Child的原型链是不是Person原型的一个实例化对象 true
Child.prototype.world = "world";
Child.prototype.sayWorld = function(){
console.log(this.world);
}
var c = new Child();
c.sayHello();
c.sayWorld();//undefined
缺点1:通过prototype 添加的方法和属性,不能用来继承!!!!为什么呢??下面有解释
- 这里对
Child.prototype = new Person();
进行解释:
- 这里分开分析:
创建一个Person原型对象实例
var obj = new Person();
Child对象的原型 指向 这个Person原型对象的实例
Child.prototype = obj;- 也就是说
Child对象的原型其实是Person原型的实例化对象
Child.prototype.__proto__ === Person.prototype;
因为子对象的原型是父对象的原型的实例化对象,所以不能继承父对象的方法和属性。
- 继承的第五种方式:混合方式, 混合了call或者apply方式、原型链方式
function Parent(hello){
this.hello = hello;
}
Parent.prototype.sayHello = function(){
console.log(this.hello);
}
function Child(hello,world){
Parent.call(this,hello);//将父类的属性继承过来
this.world = world;//新增一些属性
}
Child.prototype = new Parent();//将父类的方法继承过来
Child.prototype.sayWorld = function(){//新增一些方法
console.log(this.world);
}
var c = new Child("zhangsan","lisi");
c.sayHello();
c.sayWorld();
可以继承通过prototype 添加的方法和属性
为什么呢
因为在子对象的初始化时,加了一个父类型的原型属性回调
Parent.call(this,hello);//将父类的属性继承过来
这样就强行把父类型的原型属性拉了过来
12.call和apply的用法(详细介绍)
js中call和apply都可以实现继承,唯一的一点参数不同,
func.call(func1,var1,var2,var3)
对应的apply写法为:func.apply(func1,[var1,var2,var3])
。
- JS手册中对call的解释:
调用一个对象的一个方法,以另一个对象替换当前对象。
call([thisObj[,arg1[, arg2[, [,.argN]]]]])
参数:thisObj 可选项 默认就是this 被称作当前对象的对象
args 可选项 将被传递方法参数序列
call方法可以用来代替另一个对象调用一个方法,将一个函数的对象上下文,从初始的上下文改变为thisObj指定的新对象。也就是说这个第三方函数对象的上下文this指向的是第一方函数作用链外的那个对象的this。
说简单点,这两个函数的作用其实就是更改调用的函数对象的内部指针,即改变对象的this指向的内容,使调用的第三方函数对象的this指向作用域链外的那个对象的this。
function Person(name,age){ //定义一个类
this.name=name; //名字
this.age=age; //年龄
this.sayhello=function(){alert(this.name)};
}
function Print(){ //显示类的属性
this.funcName="Print";
this.show=function(){
var msg=[];
for(var key in this){
if(typeof(this[key])!="function"){
msg.push([key,":",this[key]].join(""));
}
}
alert(msg.join(" "));
};
}
function Student(name,age,grade,school){ //学生类
Person.apply(this,arguments);//比call优越的地方
Print.apply(this,arguments);
this.grade=grade; //年级
this.school=school; //学校
}
var p1=new Person("卜开化",80);
p1.sayhello();
var s1=new Student("白云飞",40,9,"岳麓书院");
s1.show();
s1.sayhello();
alert(s1.funcName);
//alert
卜开化
name:白云飞 age:40 funcName:Print grade:9 school:岳麓书院
白云飞
Print
另外,Function.apply()在提升程序性能方面有突出作用:
我们先从Math.max()函数说起,Math.max后面可以接任意个参数,最后返回所有参数中的最大值。
alert(Math.max(5,8)); //8
alert(Math.max(5,7,9,3,1,6)); //9
//但是在很多情况下,我们需要找出数组中最大的元素。
var arr=[5,7,9,1];
//alert(Math.max(arr)); // 这样却是不行的。NaN
//要这样写
function getMax(arr){
var arrLen=arr.length;
for(var i=0,ret=arr[0];i<arrLen;i++){
ret=Math.max(ret,arr[i]);
}
return ret;
}
alert(getMax(arr)); //9
//换用apply,可以这样写
function getMax2(arr){
return Math.max.apply(null,arr);
}
alert(getMax2(arr)); //9
//两段代码达到了同样的目的,但是getMax2却优雅,高效,简洁得多。
//再比如数组的push方法。
var arr1=[1,3,4];
var arr2=[3,4,5];
//如果我们要把 arr2展开,然后一个一个追加到arr1中去,最后让arr1=[1,3,4,3,4,5]
//arr1.push(arr2)显然是不行的。 因为这样做会得到[1,3,4,[3,4,5]]
//我们只能用一个循环去一个一个的push(当然也可以用arr1.concat(arr2),但是concat方法并不改变arr1本身)
var arrLen=arr2.length;
for(var i=0;i<arrLen;i++){
arr1.push(arr2[i]);
}
//自从有了Apply,事情就变得如此简单
Array.prototype.push.apply(arr1,arr2); //现在arr1就是想要的结果
//alert 8 9 9 9
13.原型与原型链
js 获取原型的方法?
p.__proto__
p.constructor.prototype
也就是Person.prototypeObject.getPrototypeOf(p)
14.什么是闭包,为什么要用它?
闭包就是有权访问另一个函数作用域内变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量
闭包的两个常见用途
- 在函数外部能够访问到函数内部的变量,可以创建私有变量
- 使已经执行结束的函数上下文中的变量对象继续留在内存中,因为闭包保留了这个变量对象的引用,所以这个变量对象不会被回收。
function a(){
var n = 0;
function add(){
n++;
console.log(n);
}
return add;
}
var a1 = a(); //注意,函数名只是一个标识(指向函数的指针),而()才是执行函数;
a1(); //1
a1(); //2 第二次调用n变量还在内存中
15.了解DOM和BOM吗?
- DOM 文档对象模型 把文档当作对象看待,这个对象主要定义了处理网页内容的方法和接口。
- BOM 浏览器对象模型,把浏览器当作对象看待,主要定义与浏览器进行交互的方法和接口。BOM核心是window,而window对象具有双重角色,既是通过js访问浏览器窗口的接口,又是一个全局对象,也就是在网页中定义的任何对象(变量和函数),都作为全局对象的一个属性或者方法
- window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象
16.三种事件模型是什么?
事件是用户和网页的交互
- DOM0级模型:网页中定义监听函数或者用js指定监听
- IE事件模型:
事件处理阶段:执行目标元素绑定的监听事件
事件冒泡阶段:事件从目标元素一直冒泡到document,依次检查经过的结点是否绑定了事件监听函数,有则执行
可以添加多个监听函数,按顺序依次执行 - DOM2级事件模型:
事件捕获阶段:捕获的是事件从document一直向下传播的目标元素,检查经过的结点是否绑定函数事件,如果有则执行。后面两个阶段和IE事件模型相同
17.事件委托是什么?
本质上是利用了浏览器冒泡的机制,因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因为可以把子节点的监听函数定义在父节点上,有父节点的监听函数统一处理各个子元素的事件
也就是父元素对于子元素的事件代理
不必每个元素都绑定监听事件,这样减少了内存的消耗和内存泄漏。
使用事件代理可以实现事件的动态绑定,比如新增节点就不需要为他再添加一个监听事件,发生的事件会交给父元素的监听函数来处理
18.事件传播是什么?
当事件发生再DOM元素上时,该事件并不完全发生再那个元素上
事件传播有三个阶段,
- 捕获阶段 从window开始,然后向下到每一个元素,直到到达目标元素事件或者event.target
- 目标阶段 事件已经到达目标元素
- 冒泡阶段 事件从目标元素冒泡,然后上升到每个元素,知道到达window
19.事件捕获是什么?
当事件发生再DOM元素上时,该事件并不完全发生再那个与那个元素上。在捕获阶段,事件从window开始,一直到触发事件的元素,window–document–html–body–目标元素
例如有下面的HTML结构
有三层继承关系
<div class="grandparent">
<div class="parent">
<div class="child">1</div>
</div>
</div>
我们来写一个模拟事件捕获的js代码
function addEvent(element,event,callback,isCapture = false){
//判断输入是否错误
if(!element || !event ||!callback || typeof callback !== 'function') return;
if(typeof element === 'string'){
element = document.querySelector(element);
};
element.addEventListener(event ,callback ,isCapture);
}
addEvent(document,'DOMContentLoaded',()=>{
const child = document.querySelector('.child');
const parent = document.querySelector('.parent');
const grandparent = document.querySelector('.grandparent');
addEvent(child,'click',function(e){
console.log('child');
}
);
addEvent(parent,'click',function(e){
console.log('parent');
}
);
addEvent(grandparent,'click',function(e){
console.log('grandparent');
}
);
addEvent(document,'click',function(e){
console.log('document');
}
);
addEvent('html', 'click', function (e) {
console.log('html');
})
addEvent(window, 'click', function (e) {
console.log('window');
})
});
addEventlistener方法具有第三个可选参数useCapture,默认值为false,事件将在冒泡阶段中发生,如果为true 则事件将在捕获阶段中发生,如果单击child元素,它将分别在控制台上打印window document html grandparent parent
也就是模拟出了一个事件捕获
20.事件冒泡是什么?
事件冒泡刚好与事件捕获相反,当前元素–body–html–document–window
21.DOM操作 添加移除移动复制创建和查找结点
- 创建新节点
createDocumentFragment() //创建一个DOM片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点
- 添加删除替换插入
appendChild(node)
removeChild(node)
replaceChild(new,old)
insertBefore(new,old)
- 查找
getElementById();
getElementByName();
getElementByTagName();
getElementByClassName();
querySelector();
querySelectorAll();
4.属性操作
getAttribute(key);
getAttribute(key,value);
hasAttribute(key);
document.getElementsByTagName("BUTTON")[0].hasAttribute("onclick");
removeAttribute(key)
22.js数组和字符串原生方法
数组方法
concat()返回一个由两个数组合并组成的新数组
join()返回一个由数组中的所有元素连接在一起的String对象
pop()删除数组中的最后一个元素并返回该值
push()向数组中添加新元素 返回新长度
shift()删除数组中的第一个元素并返回该值
unshift返回一个数组,在该数组头部插入指定的元素
sort()返回一个元素被排序了的Array对象
reverse()返回一个元素反序的Array对象
splice(index.num,foo...)返回数组的一个片段 --剪切
slice(start,end)复制 [start,end) 负数的时候 lenght+start/end
字符串方法
length 属性 长度
concat(String) 连接两个或更多个字符
indexOf(string) 返回出现字符的位置
substr(num1,[num2])截取字符串
toLowerCase()转成小写
toUpperCase()转成大写
replace(str1,str2) 字符串替换
math
cell(数值)大于或等于该数的最小整数
floor(数值)小于或等于该数的最大整数
min(数值1,数值2)返回最小值
max(数值1,数值2)返回最大值
pow(数值1,数值2)返回数值1的数值2的次方
random()返回随机数0--1
round(数值)四舍五入
sqrt(数值)开平方根
23.常用正则表达式
//(1)匹配 16 进制颜色值
var color = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
//(2)匹配日期,如 yyyy-mm-dd 格式
var date = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
//(3)匹配 qq 号
var qq = /^[1-9][0-9]{4,10}$/g;
//(4)手机号码正则
var phone = /^1[34578]\d{9}$/g;
//(5)用户名正则
var username = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
//(6)Email正则
var email = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/;
//(7)身份证号(18位)正则
var cP = /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/;
//(8)URL正则
var urlP= /^((https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
// (9)ipv4地址正则
var ipP = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
// (10)//车牌号正则
var cPattern = /^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领A-Z]{1}[A-Z]{1}[A-Z0-9]{4}[A-Z0-9挂学警港澳]{1}$/;
// (11)强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):var pwd = /^(?=.\d)(?=.[a-z])(?=.[A-Z]).{8,10}$/
24.Ajax 是什么? 如何创建一个 Ajax?
一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。
- 原生手写 5 个步骤
创建XMLHttpRequest异步对象
var xhr = new XMLHttpRequest()
设置回调函数
xhr.onreadystatechange = callback
使用open方法与服务器建立连接
// get 方式
xhr.open("get", "test.php", true)
// post 方式发送数据 需要设置请求头
xhr.open("post", "test.php", true)
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
向服务器发送数据
// get 不需要传递参数
xhr.send(null)
// post 需要传递参数
xhr.send("name=jay&age=18")
在回调函数中针对不同的响应状态进行处理
function callback() {
// 判断异步对象的状态
if(xhr.readyState == 4) {
// 判断交互是否成功
if(xhr.status == 200) {
// 获取服务器响应的数据
var res = xhr.responseText
// 解析数据
res = JSON.parse(res)
}
}
}
- jQuery写法
$.ajax({
type:'post',
url:'',
async:ture,//async 异步 sync 同步
data:data,//针对post请求
dataType:'jsonp',
success:function (msg) {
},
error:function (error) {
}
})
- promise 封装实现:
// 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;
}
25. js 延迟加载的方式有哪些?
js 的加载、解析和执行会阻塞页面的渲染过程,因此我们希望 js 脚本能够尽可能的延迟加载,提高页面的渲染速度。
- 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。
- 给 js 脚本添加 defer属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
- 给 js 脚本添加 async属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
- 动态创建 DOM 标签的方式,我们可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
26.谈谈你对模块化开发的理解?
- 一个模块是实现一个特定功能的一组方法。在最开始的时候,js 只实现一些简单的功能,所以并没有模块的概念,但随着程序越来越复杂,代码的模块化开发变得越来越重要。
- 由于函数具有独立作用域的特点,最原始的写法是使用函数来作为模块,几个函数作为一个模块,但是这种方式容易造成全局变量的污染,并且模块间没有联系。
- 后面提出了对象写法,通过将函数作为一个对象的方法来实现,这样解决了直接使用函数作为模块的一些缺点,但是这种办法会暴露所有的所有的模块成员,外部代码可以修改内部属性的值。
- 现在最常用的是立即执行函数的写法,通过利用闭包来实现模块私有作用域的建立,同时不会对全局作用域造成污染。
27.js 的几种模块规范?模块加载方案?
- 第一种是 CommonJS 方案,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
- 第二种是 AMD 方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范
- 第三种是 CMD 方案,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。它和require.js的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
- 第四种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块
28.AMD 和 CMD 规范的区别?
- 第一个方面是在模块定义时对依赖的处理不同。AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块。而 CMD 推崇就近依赖,只有在用到某个模块的时候再去 require。
- 第二个方面是对依赖模块的执行时机处理不同。首先 AMD 和 CMD 对于模块的加载方式都是异步加载,不过它们的区别在于模块的执行时机,AMD 在依赖模块加载完成后就直接执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致。而 CMD在依赖模块加载完成后并不执行,只是下载而已,等到所有的依赖模块都加载好后,进入回调函数逻辑,遇到 require 语句的时候才执行对应的模块,这样模块的执行顺序就和我们书写的顺序保持一致了。
// CMD
define(function(require, exports, module) {
var a = require("./a");
a.doSomething();
// 此处略去 100 行
var b = require("./b"); // 依赖可以就近书写
b.doSomething();
// ...
});
// AMD 默认推荐
define(["./a", "./b"], function(a, b) {
// 依赖必须一开始就写好
a.doSomething();
// 此处略去 100 行
b.doSomething();
// ...
})
29.ES6 模块与 CommonJS 模块、AMD、CMD 的差异。
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS 模块输出的是值的,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值
CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。CommonJS 模块就是对象,即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
30. requireJS的核心原理是什么?
require.js 的核心原理是通过动态创建 script 脚本来异步引入模块,然后对每个脚本的 load 事件进行监听,如果每个脚本都加载完成了,再调用回调函数。
31.谈谈JS的运行机制
-
1.单线程
因为是 脚本语言 和 要操作DOM 就决定了他是单线程
因为浏览器为了避免JS同时两个或多个线程,例如:一个线程在DOM节点上增加内容,另一个线程删除了某节点; -
2.事件循环
同步任务:例如页面构建DOM树和元素渲染
异步任务:像加载图片音乐之类占用资源大耗时久的任务
- 任务会分别进入不同的执行场所,同步的进入主线程,异步的进入Event Table并注册回调函数
- 当指定事情完成了Event Table就会讲函数移入Event Queue阻塞,等待回调
- 当主线程任务完成,就会读取EventQueue相关函数,进入主线程执行
- 不断重复 就成了事件循环
那么主线程什么时候为空呢?
js引擎存在monitoring process进程, 会不断检查主线程执行栈是否为空,空了就会去阻塞函数进行回调
需要注意的是,除了同步和异步,还可以分为宏任务、微任务,JS引擎优先执行微任务
- 微任务包括:promise的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。
- 宏任务包括: script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作、UI 渲染等。
面试的推荐回答
- 首先js是单线程执行的,代码执行的时候通过把不同函数的执行上下文压入执行栈中,以保证代码有序执行
- 执行代码的时候,如果遇到异步事件,js不会一直等结果,而是阻塞,继续执行主线程执行栈的其他任务
- 当执行栈中的任务执行完毕过后,再来对阻塞的函数进行回调,在当前任务队列不一样的另一个任务队列中等待执行
- 任务队列可以分为微任务和宏任务队列,当执行栈中的事件执行完毕时后,js引擎首先判断微任务中是否有任务执行,如果有就压入栈中执行
- 当微任务做完过后才开始做宏任务
例如以下代码:
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function(resolve, reject) {
console.log(2);
resolve()
}).then(function() {
console.log(3)
});
process.nextTick(function () {
console.log(4)
})
console.log(5)
第一轮
- 主线程开始执行,遇到setTimeout,讲回调函数丢进宏任务队列中,
- 再往下执行new Promise 立即执行,输出2;
- then的回调函数丢进微任务队列中;
- process.nextTick,同样将回调函数扔到为任务队列,再继续执行,输出5
- 当所有同步任务执行完成过后,看有没有可以执行的微任务,发现之前.then()和.nextTick()
- 判断哪个先执行,process.nextTick指定的异步任务总是发生在所有异步任务之前,因此先执行process.nextTick输出4然后执行then函数输出3,第一轮执行结束
第二轮
- 从宏任务开始发现setTimeout回调,输出1 执行完毕。
最终结果是:25431
32.arguments 的对象是什么?
是函数中传递的参数集合。是类似数组的对象,因为他有length属性,可以用数组索引法arguments[0]来访问某个值
但他没有数组的内置方法,如:forEach、reduce、filter、map
可以使用Array.prototype.slice.call(arguments)
,转换成一个数组
注意 剪头函数 没有 arguments 对象
function one() {
return arguments;
}
const two = function () {
return arguments;
}
const three = function three() {
return arguments;
}
const four = () => arguments;
four(); // Throws an error - arguments is not defined
使用rest解构语法,可以解决这个问题。
const four = (...args) => args;
自动将参数值放入数组中
33.为什么在调用这个函数时,代码中的b会变成一个全局变量?
function myFunc(){
let a = b = 0;
}
myFunc();
实际上是
function myFunc() {
let a = (b = 0);
}
myFunc();
原因是赋值运算符是从右到左求值的,当多个赋值运算符出现在一个表达式中时,它们从右向左求值。
- 首先,表达式b = 0求值,在本例中没有声明,因此,JS引擎在window对象中创建了一个全局变量b作为属性,然后返回值为0,并赋给新的局部变量a
我们可以通过在赋值之前声明变量来解决这个全局污染的问题
function myFunc() {
let a,b;
a = b = 0;
}
myFunc();
34.简单介绍一下V8引擎的垃圾回收机制
基于分代回收机制,基于世代假说,这个假说有两个特点,一是新生的对象容易早死,另一个是不死的对象会活得更久
-
新生代
新创建的对象或者之经历过一次的垃圾回收对象被称为新生代,经历过多次垃圾回收的对象被称为老生代
新生代被分为From和To两个空间,To一般是闲置的。From空间满了的会执行Scavenge算法进行垃圾回收。当我们执行垃圾回收算法的时候,应用逻辑就会停止,等垃圾回收结束后再继续执行 -
Scavenge算法
首先检查From空间的存活对象,如果对象存活则判断对象是否满足晋升到老生代的条件,如果满足条件则晋升到老生代。如果不满足条件则移动 To 空间
如果对象不存活,则释放对象空间
最后将From空间和To空间角色进行交换 -
晋升有两个条件
1.判断对象是否经历过一次回收,如果经历过就从From空间复制到老生代中,没有经历就复制到To空间
2.To空间的内存使用占比是否超过限制,当对象从From空间复制到To空间,如果To空间超过25%就是说要满了,那么直接晋升到老生代中(草草草真就被卷走了呗),因为算法结束过后两个空间会交换位置,如果内存太小会影响后续的内存分配。 -
老生代
采用标记清楚法和标记压缩法。首先会对内存中存活的对象进行标记,标记结束后清除掉那些没有标记的对象。由于标记清楚后会造成很多的内存碎片。不便于后面的内存分配,所以为了解决内存碎片引入了标记压缩法
由于再进行垃圾回收的时候会暂停应用的逻辑,对于新生代方法由于内存小,每次停顿的时间不会太长,但对于老生代来说每次垃圾回收的时间长,停顿会造成很大的影响,为了解决这个问题V8引入了增量标记的方法,讲一次停顿进行的过程分成了多步,完成了一小步就让运行逻辑执行一会儿,这样就能交替运行
35.哪些操作会造成内存泄漏
- 意外的全局变量
- 被遗忘的计时器或回调函数
- 脱离DOM的引用
- 闭包
- 未声明的变量就直接是window对象的属性了,或者意外的创建了一个全局变量,而使这个变量一直留在内存中不能回收
- 定时器setInterval,忘记取消它,如果对外部变量有引用的话,那么这个变量就一直要在内存中,无法被回收
- DOM元素的引用,如果这个元素被删除了,但是我们一直保留了这个元素的引用,所以也不能回收,解决方式:空指针null
- 不合理的使用闭包,导致某些变量一直被存在内存中
36.ECMAScript是什么?
ECMAScript 是编写脚本语言的标准,这意味着JavaScript遵循ECMAScript标准中的规范变化,因为它是JavaScript的蓝图。
ECMAScript 和 Javascript,本质上都跟一门语言有关,一个是语言本身的名字,一个是语言的约束条件 只不过发明JavaScript的那个人(Netscape公司),把东西交给了ECMA(European Computer Manufacturers Association),这个人规定一下他的标准,因为当时有java语言了,又想强调这个东西是让ECMA这个人定的规则,所以就这样一个神奇的东西诞生了,这个东西的名称就叫做ECMAScript。
javaScript = ECMAScript + DOM + BOM(自认为是一种广义的JavaScript)
ECMAScript说什么JavaScript就得做什么!
JavaScript(狭义的JavaScript)做什么都要问问ECMAScript我能不能这样干!如果不能我就错了!能我就是对的!——突然感觉JavaScript好没有尊严,为啥要搞个人出来约束自己,
37.ES6有哪些新特性?
- 块作用域
- 类
- 箭头函数
- 模板字符串
- 加强的对象字面
- 对象解构
- Promise
- 模块
- Symbol 独一无二的值 不会与其他属性名冲突
- 代理(proxy)Set
- 函数默认参数
- rest和展开
38.var let const 区别
var 声明的变量会挂载在window上,而let和const声明的变量不会
var a = 100;
console.log(a,window.a); // 100 100
let b = 10;
console.log(b,window.b); // 10 undefined
const c = 1;
console.log(c,window.c); // 1 undefined
var声明变量存在变量提升(变量可以先使用再声明),let和const不存在变量提升(
use strict;
严格模式下就不行哦 )
console.log(a); // undefined ===> a已声明还没赋值,默认得到undefined值
var a = 100;
console.log(b); // 报错:b is not defined ===> 找不到b这个变量
let b = 10;
console.log(c); // 报错:c is not defined ===> 找不到c这个变量
const c = 10;
let和const声明形成块作用域 var挂载在window
if(1){
var a = 100;
let b = 10;
}
console.log(a); // 100
console.log(b) // 报错:b is not defined ===> 找不到b这个变量
-------------------------------------------------------------
if(1){
var a = 100;
const c = 1;
}
console.log(a); // 100
console.log(c) // 报错:c is not defined ===> 找不到c这个变量
同一作用域下let和const不能声明同名变量,而var可以
var a = 100;
console.log(a); // 100
var a = 10;
console.log(a); // 10
-------------------------------------
let a = 100;
let a = 10;
// 控制台报错:Identifier 'a' has already been declared ===> 标识符a已经被声明了。
暂存死区
var a = 100;
if(1){
a = 10;
//在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域找变量a,
// 而这时,还未到声明时候,所以控制台Error:a is not defined
let a = 1;
}
const
/*
* 1、一旦声明必须赋值,不能使用null占位。
*
* 2、声明后不能再修改
*
* 3、如果声明的是复合类型数据(引用数据类型),可以修改其属性
*
* */
const a = 100;
const list = [];
list[0] = 10;
console.log(list); // [10]
const obj = {a:100};
obj.name = 'apple';
obj.a = 10000;
console.log(obj); // {a:10000,name:'apple'}
39.什么是箭头函数?
箭头函数没有自己的this,arguments,super或new.target
,更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数
//ES5 Version
var getCurrentDate = function (){
return new Date();
}
//ES6 Version
const getCurrentDate = () => new Date();
只需要()括号,不需要 return 语句
//ES5 Version
function greet(name) {
return 'Hello ' + name + '!';
}
//ES6 Version
const greet = (name) => `Hello ${name}`;
const greet2 = name => `Hello ${name}`;
使用与函数表达式和函数声明相同的参数
箭头函数不能访问arguments对象。所以调用第一个getArgs函数会抛出一个错误。相反,我们可以使用rest参数来获得在箭头函数中传递的所有参数。
const getArgs = () => arguments
const getArgs2 = (...rest) => rest
const data = {
result: 0,
nums: [1, 2, 3, 4, 5],
computeResult() {
// 这里的“this”指的是“data”对象
const addAll = () => {
return this.nums.reduce((total, cur) => total + cur, 0)
};
this.result = addAll();
}
};
箭头函数没有自己的this值。它捕获词法作用域函数的this值,在此示例中,addAll函数将复制computeResult 方法中的this值,如果我们在全局作用域声明箭头函数,则this值为 window 对象。
40.什么是类?
类(class)是在 JS 中编写构造函数的新方法。它是使用构造函数的语法糖,在底层中使用仍然是原型和基于原型的继承。
//ES5 Version
function Person(firstName, lastName, age, address){
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
this.address = address;
}
Person.self = function(){
return this;
}
Person.prototype.toString = function(){
return "[object Person]";
}
Person.prototype.getFullName = function (){
return this.firstName + " " + this.lastName;
}
//ES6 Version
class Person {
constructor(firstName, lastName, age, address){
this.lastName = lastName;
this.firstName = firstName;
this.age = age;
this.address = address;
}
static self() {
return this;
}
toString(){
return "[object Person]";
}
getFullName(){
return `${this.firstName} ${this.lastName}`;
}
}
重写方法并从另一个类继承。
//ES5 Version
Employee.prototype = Object.create(Person.prototype);
function Employee(firstName, lastName, age, address, jobTitle, yearStarted) {
Person.call(this, firstName, lastName, age, address);
this.jobTitle = jobTitle;
this.yearStarted = yearStarted;
}
Employee.prototype.describe = function () {
return `I am ${this.getFullName()} and I have a position of ${this.jobTitle} and I started at ${this.yearStarted}`;
}
Employee.prototype.toString = function () {
return "[object Employee]";
}
//ES6 Version
class Employee extends Person { //Inherits from "Person" class
constructor(firstName, lastName, age, address, jobTitle, yearStarted) {
super(firstName, lastName, age, address);
this.jobTitle = jobTitle;
this.yearStarted = yearStarted;
}
describe() {
return `I am ${this.getFullName()} and I have a position of ${this.jobTitle} and I started at ${this.yearStarted}`;
}
toString() { // Overriding the "toString" method of "Person"
return "[object Employee]";
}
}
所以我们要怎么知道它在内部使用原型?
class Something {
}
function AnotherSomething(){
}
const as = new AnotherSomething();
const s = new Something();
console.log(typeof Something); // "function"
console.log(typeof AnotherSomething); // "function"
console.log(as.toString()); // "[object Object]"
console.log(as.toString()); // "[object Object]"
console.log(as.toString === Object.prototype.toString); // true
console.log(s.toString === Object.prototype.toString); // true
41.什么是模板字符串?
模板字符串是在 JS 中创建字符串的一种新方法。
我们可以通过使用反引号使模板字符串化。
//ES5 Version
var greet = 'Hi I\'m Mark';
//ES6 Version
let greet = `Hi I'm Mark`;
在 ES5 中我们需要使用一些转义字符来达到多行的效果,在模板字符串不需要这么麻烦:
//ES5 Version
var lastWords = '\n'
+ ' I \n'
+ ' Am \n'
+ 'Iron Man \n';
//ES6 Version
let lastWords = `
I
Am
Iron Man
`;
在 ES5 版本中,如果需要在字符串中添加表达式或值,则需要使用+运算符。在模板字符串s中,我们可以使用${expr}嵌入一个表达式,这使其比 ES5 版本更整洁
//ES5 Version
function greet(name) {
return 'Hello ' + name + '!';
}
//ES6 Version
function greet(name) {
return `Hello ${name} !`;
}
42.什么是对象解构?
假设有如下的对象
const employee = {
firstName: "Marko",
lastName: "Polo",
position: "Software Developer",
yearHired: 2017
};
es5之前获取对象属性时
var firstName = employee.firstName;
var lastName = employee.lastName;
var position = employee.position;
var yearHired = employee.yearHired;
使用解构就方便多了
{ firstName, lastName, position, yearHired } = employee;
我们还可以为属性取别名:
let { firstName: fName, lastName: lName, position, yearHired } = employee;
当然如果属性值为 undefined 时,我们还可以指定默认值:
let { firstName = "Mark", lastName: lName, position, yearHired } = employee;
43.什么是Set对象,它是如何工作的?
Set 对象允许你存储任何类型的唯一值
,无论是原始值或者是对象引用。
我们可以使用Set构造函数创建Set实例。
const set1 = new Set();
const set2 = new Set(["a","b","c","d","d","e"]);
使用add方法向Set实例中添加一个新值,因为add方法返回Set对象,所以我们可以以链式的方式再次使用add,如果一个值已经存在于Set对象中,那么它将不再被添加。
set2.add("f");
set2.add("g").add("h").add("i").add("j").add("k").add("k");
// 后一个“k”不会被添加到set对象中,因为它已经存在了
我们可以使用has方法检查Set实例中是否存在特定的值。
set2.has("a") // true
set2.has("z") // true
我们可以使用size属性获得Set实例的长度。
set2.size // returns 10
可以使用clear方法删除 Set 中的数据。
set2.clear();
我们可以使用Set对象来删除数组中重复的元素。
const numbers = [1, 2, 3, 4, 5, 6, 6, 7, 8, 8, 5];
const uniqueNums = [...new Set(numbers)]; // [1,2,3,4,5,6,7,8]
另外还有WeakSet, 与 Set 类似,也是不重复的值的集合。但是 WeakSet 的成员只能是
对象
,而不能是其他类型的值。WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet对该对象的引用
。
44.什么是Proxy?
对要保护的对线套一层,不被外界所访问。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截
,因此提供了一种机制,可以对外界的访问进行过滤和改写
let obj={
time:'2017-03-11',
name:'net',
_r:123
};
let monitor=new Proxy(obj,{
// 拦截对象属性的读取
get(target,key){
return target[key].replace('2017','2018')
},
// 拦截对象设置属性
set(target,key,value){
if(key==='name'){
return target[key]=value;
}else{
return target[key];
}
},
// 拦截key in object操作
has(target,key){
if(key==='name'){
return target[key]
}else{
return false;
}
},
// 拦截delete
deleteProperty(target,key){
if(key.indexOf('_')>-1){
delete target[key];
return true;
}else{
return target[key]
}
},
// 拦截Object.keys,Object.getOwnPropertySymbols,Object.getOwnPropertyNames
ownKeys(target){
return Object.keys(target).filter(item=>item!='time')
}
});
console.log('get',monitor.time);
monitor.time='2018';
monitor.name='poetries';
console.log('set',monitor.time,monitor);
console.log('has','name' in monitor,'time' in monitor);
// delete monitor.time;
// console.log('delete',monitor);
//
// delete monitor._r;
// console.log('delete',monitor);
console.log('ownKeys',Object.keys(monitor));
45.写一个通用的事件侦听器函数
const EventUtils = {
// 视能力分别使用dom0||dom2||IE方式 来绑定事件
// 添加事件
addEvent: function(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler;
}
},
// 移除事件
removeEvent: function(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else if (element.detachEvent) {
element.detachEvent("on" + type, handler);
} else {
element["on" + type] = null;
}
},
// 获取事件目标
getTarget: function(event) {
return event.target || event.srcElement;
},
// 获取 event 对象的引用,取到事件的所有信息,确保随时能使用 event
getEvent: function(event) {
return event || window.event;
},
// 阻止事件(主要是事件冒泡,因为 IE 不支持事件捕获)
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
},
// 取消事件的默认行为
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
};
46.什么是函数式编程?JS有哪些特性使其成为函数式语言
函数式编程(通常缩写为FP)是通过编写纯函数,避免共享状态、可变数据、副作用 来构建软件的过程。数式编程是声明式 的而不是命令式 的,应用程序的状态是通过纯函数流动的。与面向对象编程形成对比,面向对象中应用程序的状态通常与对象中的方法共享和共处。
函数式编程是一种编程范式 ,这意味着它是一种基于一些基本的定义原则(如上所列)思考软件构建的方式。当然,编程范式的其他示例也包括面向对象编程和过程编程。
函数式的代码往往比命令式或面向对象的代码更简洁,更可预测,更容易测试 - 但如果不熟悉它以及与之相关的常见模式,函数式的代码也可能看起来更密集杂乱,并且 相关文献对新人来说是不好理解的。
47.什么是高阶函数?
- 高阶函数只是将函数作为参数或返回值的函数
function higherOrderFunction(param,callback){
return callback(param);
}
48.为什么函数被称为一等公民?
函数不仅拥有一切传统函数的使用方式(声明和调用),而且可以做到像简单值一样
- 赋值:
var func = function(){}
- 传参:
function func(x,callback){callback(x);}
- 返回:
function(){return function(){}}
这样的函数被称为第一级函数。
很多函数都充当了类的构造函数的作用,同时又是一个Function类的实例。
49.手动实现Array.prototype.map()方法
map()方法创建一个新数组,其结果是该数组的每个元素都调用一个提供的函数后返回的结果
- 因为是对数组操作,所以最开始要判断是否是数组
- 因为要返回新的数组,所以要在函数内定义新的数组并返回
- 因为要对传入的数组的每个元素进行传入的那个函数的操作并返回操作后的新数组,所以就在最开始定义的那个数组中进行挨个push
function map(arr,mapCallback){
//首先检查传递的是不是array和callback函数
if(!Array.isArray(arr) || !arr.length || typeof mapCallback !== 'function'){
return [];
}else{
//每次调用此函数时,都会创建一个result数组,并返回;因为map就是不会改变原数组,就像bind一样
let result = [];
for(let i = 0, len = arr.length; i < len; i++){
//map方法作用还有将会返回数组每个被mapCallback处理过后的值,所以在新建的result数组中挨个push
result.push(mapCallback(arr[i],u,arr));//数组元素 元素索引 原数组
}
return result;
}
}
50.手动实现Array.prototype.filter()方法
filter()方法创建一个新的过滤后的数组,包含通过所提供的函数实现的测试的所有元素
过滤的意思是:满足某个条件就返回 没满足就去除
function filter(arr,filterCallback){
//首先检查传递的参数是否是有长度的数组和过滤函数
if(!Array.isArray(arr) || !arr.length || typeof filterCallback !== 'function'){ return [];}
else{
//因为创建新数组所以块级作用域新建
let result = [];
for(let i = 0, len = arr.length, i++){
//检查filterCallback 返回值是否为真值 也就是传入的参数是否满足过滤条件
if(filterCallback(arr[i], i, arr)){
result.push(arr[i]);
}
}
return result;
}
}
51.手动实现Array.prototype.reduce()方法
reduce()方法对数组的每个元素执行一个提供的reducer函数(升序执行),将结果汇总为单个返回值
function reduce(arr, reduceCallback, initialValue){
//首先,检查传递的参数是否正确
if(!Array.isArray(arr) || !arr.length || typeof reduceCallback !== 'function'){ return []; }
else{
//如果没有将initialValue传递给该函数,我们将使用第一个数组项作为initial Value
let hasInitialValue = initialValue !== undefined;
let value = hasInitialValue ? initialValue : arr[0];
}
//如果有传递inisialValue,则索引从1开始,否则从0开始
for(let i = hasInitialValue ? 1 : 0,len = arr.length; i < len; i
++){
value = reduceCallback(value, arr[i], i, arr);
}
return value;
}
52.JS的深浅拷贝
-
浅拷贝:
创建一个新对象,这个对象有原始对象属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值;
如果是引用类型,拷贝的就是内存地址;
所以如果其中一个对象改变了这个地址,就会影响到另一个对象。 -
深拷贝:
将一个对象从内存中完整的拷贝一份出来,从堆内存开辟新的区域存放新对象,且修改新对象不会影响原对象,
浅拷贝实现方式
- Object.assign()方法:
用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。将返回目标对象。 - Array.prototype.slice():
slice()方法返回一个新的数组对象,这个对象是一个由begin和end(不包括end)决定的原数组的浅拷贝,原数组不会被改变。 - 扩展运算符(…返回的就是新的对象)
let a = {
name:'Sirius',
flag:{
title:"better day by day",
time:"2020-12-09"
}
}
let b = {...a};
- 纯手写实现基本数据类型和引用数据类型指针的拷贝
var a = {
key1:"111"
}
function copy(p){
// console.log(p)
var c = {};
for(var i in p){
c[i] = p[i];
// console.log(i) // key1
}
return c;
}
// copy(a);
a.key2 = ["you","me"];
var b = copy(a);
b.key3 = "ours";
console.log(a.key3); // undefined
console.log(b.key3) // ours
// console.log(b) // {key1:"111",key2:["you","me"]}
b.key2.push("us")
console.log(b.key2); // ["you", "me", "us"]
console.log(a.key2); // ["you", "me", "us"]
深拷贝的实现方式
- 乞丐版:JSON.parse(JSON.stringify(object))
缺点:会忽略undefined、symbol、函数;不能解决循环引用;不能处理正则、new Date() - 基础版(面试够用)浅拷贝+递归(只考虑了普通的object和array两种数据类型)
function cloneDeep(target, map = new WeakMap()){
//判断是不是object
if(typeof target === 'object'){
//判断是不是数组或object
let cloneTarget = Array.isArray(target) ? [] : {};
//如果取得到值
if(map.get(target)){
return target;
}
//取不到就说明这个对象里面的target还是对象,就是这个对象不是一维的要进行维度展开
map.set(target,cloneTarget);
for(const key in target){
cloneTarget[key] = cloneDeep(target[key],map);
}
return cloneTarget;
}
}
- 终极版(用到调用就行。。。)
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
function getType(target) {
return Object.prototype.toString.call(target);
}
function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}
function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}
function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}
function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}
function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
case funcTag:
return cloneFunction(targe);
default:
return null;
}
}
function clone(target, map = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}
// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
} else {
return cloneOtherType(target, type);
}
// 防止循环引用
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value, map));
});
return cloneTarget;
}
// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value, map));
});
return cloneTarget;
}
// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});
return cloneTarget;
}
module.exports = {
clone
};
53.手写call apply bind
FE-interview
- 我们先看FE-interview上的call函数实现方式
call函数实现步骤:
- 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用call等方式调用的情况
- 判断传入上下文对象是否存在,如果不存在,则设置为window
- 处理传入的参数,截取第一个参数后的所有参数
- 将函数作为上下文对象的一个属性
- 使用上下文对象来调用这个方法,并保存返回的结果
- 删除刚才新增的属性
- 返回结果
//call函数实现
Function.prototype.myCall = function(context){
//判断调用对象
if(typeof this !== 'function'){
console.error("type error");
return null;
}
//获取参数
let args = [...arguments].slice(1),
result = null;
//判断context是否传入 如果未传入 则设置为window
context = context || window;
//将调用函数设为对象的方法
context.fn = this;
//调用函数
result = context.fn(...args);
//将属性删除,因为只需要结果
delete context.fn;
return result;
}
apply函数实现步骤
-
判断调用对象是否是函数对象,即使我们是定义在函数的原型上的,到那时可能出现使用call等方式调用的情况
-
判断传入上下文对象是否存在,如果不存在,则设置为window
-
将函数作为上下文对象的一个属性
-
判断参数值是否传入
-
使用上下文对象来调用这个方法,并保存返回结果
-
删除刚才在上下文对象中新建的这个属性,因为只要结果并不想在上下文上增加
-
返回结果
//apply函数实现
Function.prototype.myApply = function(context){
//判断调用对象是否是函数
if(typeof this !== 'function'){
throw new TypeError("Error");
}
let result = null;
//判断context是否存在,如果没有传入则挂载到window
context = context || window ;
//给上下文对象建立一个指向此方法的指针
context.fn = this;
//调用方法
if(arguments[1]){
result = context.fn(...arguments[1]);
}else{
result = context.fn();
}
//删除这个指针
delete context.fn();
return result;
}
- bind函数实现步骤
- 判断调用对象是否是函数,即使我们是定义在函数的原型上的,但是可能出现使用call等方式调用的情况
- 保存当前函数的引用,获取其余传入的参数值
- 创建一个函数返回
- 函数内部使用apply来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的this给apply调用,其余情况都传入指定的上下文对象
//bind函数实现
Function.prototype.myBind = function(context){
//判断调用对象是否是函数
if(typeof this !== 'function'){
throw new TypeError("Error");
}
//获取参数
let args = [...arguments].slice(1),
fn = this;
return function Fn(){
//根据调用方式 传入不同绑定值
return fn.apply(
this instanceof Fn ? this : context,
args.concat(...arguments)
)
}
}
博主自己的方式(我觉得比FE-interview更好理解)
- bind
bind的特点:
- 接受调用传参和新建对象构造函数传参
- 如果是外部没有传入this就要新建一个this
- 和call接受的参数和实现的回调功能一样
- 返回的是一个新创建的原来传入的this克隆的对象
//函数的构造函数 : Function
//用es6解构rest的方式传参
Function.prototype.myBind = function(objThis,...params){
//定义指向当前函数this的指针
const thisTn = this;
//因为最终返回的是一个函数,所以声明一个函数返回
//用let表示块作用域
let funcForBind = function(...secondParams){
/*因为例如
let sayClone = obj.say.bind(obj2,1,2,3);
sayClone(4);
这里还是可以进行传参,最终传的就是1,2,3,4,所以可以用解构 ...secondParams
*/
//判断是不是新创建的对象 如果新创建的对象的话 this就是当前函数的this 不是新创建的话就是传进来的那个对象的上下文
const isNew = this instanseof funcForBind;
const thisArg = isNew ? this : objThis;
//返回调用 并分别解构传入的参数params和创建对象传入的参数secondParams
return thisFn.call(thisArg,...params,...secondParams);
}
//因为bind返回的是克隆的对象,所以还要把原型链进行克隆
funForBind.prototype = Object.create(thisFn.prototype);
return funcForBind;
}
- call 和 apply
之前的例子:
//给obj2增加一个obj的call的函数,然后用传入的参数进行调用返回最终值
obj.say.call(obj2,1,2,3);
Function.prototype.myCall = function(thisArg,...arr){
if(thisArg == null || thisArg == undefined){
thisArg = window;
}
//定义一个不重复的方法名称
const specialMethod = Symbol('anything');
//将这个不重复的方法Function的指针给thisArg的specialMethod方法
thisArg[specialMethod] = this;
//调用函数并结果返回
let result = thisArg[specialMethod](...arr);
//delete 新增的属性
delete thisArg[specialMethod];
return result;
}
obj.say.myCall(obj2,1,2,3);
Function.prototype.myApply = function(thisArg,arr){
if(thisArg == null || thisArg == undefined){
thisArg = window;
}
//定义一个不重复的方法
const specialMethod = Symbol('anything');
//将这个不重复的方法的指针给thisArg的specialMethod方法
thisArg[specialMethod] = this;
//调用函数并结果返回
let result = thisArg[specialMethod](...arr);
//delete 新增的属性
delete thisArg[specialMethod];
return result;
}
obj.say.myApply(obj2,[1,2,3]);
54.函数柯里化的实现
- 函数柯里化 指的是 一种将使用多个参数的一个函数 转换成 一系列使用一个参数的函数的技术
- 感觉像是给函数的解构
function curry(fn, args){
//获取函数需要的参数长度
let length = fn.length;
args = args || [];
return function(){
let subArgs = args.slice(0);
//拼接所有参数,不仅是curry的,还有fn的
for(let i = 0; i < arguments.length; i++){
subArgs.push(arguments[i]);
}
//判断参数的长度是否已经满足函数所需参数的长度
if(subArgs.length >= length){
//如果满足,执行函数
return fn.apply(this, subArgs);
}else{
//如果不满足,递归返回柯里化的函数,等待参数的传入
return curry.call(this, fn, subArgs);
}
};
}
//es6实现
function curry(fn, ...args){
return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}
55.js模拟new操作符的实现
创建一个用户定义的对象类型的实例 或 具有构造函数的内置对象的实例
new关键字会进行如下的操作:
- 创建一个空的简单JS对象 即
{}
- 链接该对象(即设置该对象的构造函数)到另一个对象
- 将步骤1新建的对象作为this的上下文
- 如果该函数没有返回对象 则返回this
第一步:创建一个简单空对象
var obj = {}
第二步:链接该对象到另一个对象(原型链)
// 设置原型链
obj.__proto__ = Dog.prototype
第三步:将步骤1新创建的对象作为 this 的上下文
// this指向obj对象
Dog.apply(obj, ['大黄', 'yellow', 3])
第四步:如果该函数没有返回对象,则返回this
// 因为 Dog() 没有返回值,所以返回obj
var dog = obj
dog.getName() // '大黄'
需要注意的是如果 Dog() 有 return 则返回 return的值
var rtnObj = {}
function Dog(name, color, age) {
// ...
//返回一个对象
return rtnObj
}
var dog = new Dog('大黄', 'yellow', 3)
console.log(dog === rtnObj) // true
接下来我们将以上步骤封装成一个对象实例化方法,即模拟new的操作:
function objectFactory(){
var obj = {};
//取得该方法的第一个参数(并删除第一个参数),该参数是构造函数
var Constructor = [].shift.apply(arguments);
//将新对象的内部属性__proto__指向构造函数的原型,这样新对象就可以访问原型中的属性和方法
obj.__proto__ = Constructor.prototype;
//取得构造函数的返回值
var ret = Constructor.apply(obj, arguments);
//如果返回值是一个对象就返回该对象,否则返回构造函数的一个实例对象
return typeof ret === "object" ? ret : obj;
}
56.什么是回调函数?回调函数有什么缺点?
- 回调函数是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段(回调函数)代码
- 在js中,函数也是对象的一种,同样对象可以作为参数传递给函数,因此**函数也可以作为参数传递给另一个函数,**这个作为参数的函数就是回调函数。
const btnAdd = document.getElementById('btnAdd');
btnAdd.addEventListener('click', function clickCallback(e) {
// do something useless
});
在本例中,我们等待id为btnAdd的元素中的click事件,如果它被单击,则执行clickCallback函数。回调函数向某些数据或事件添加一些功能。
回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个事件存在依赖性:
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
},3000)
},2000)
},1000)
这就是典型的回调地狱,以上代码看起来不利于阅读和维护,事件一旦多起来就更是乱糟糟,所以在es6中提出了Promise和async/await来解决回调地狱的问题。当然,回调函数还存在着别的几个缺点,比如不能使用 try catch 捕获错误,不能直接 return。接下来的两条就是来解决这些问题的,咱们往下看。
57.Promise是什么,可以手写实现一下吗?
Promise,翻译过来是承诺,承诺它过一段时间会给你一个结果。从编程讲Promise 是异步编程的一种解决方案。下面是Promise在MDN的相关说明:
Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象。
一个 Promise有以下几种状态:
- pending: 初始状态,既不是成功,也不是失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 fulfilled/rejected 后,就不能再次改变。可能光看概念大家不理解Promise,我们举个简单的栗子;
我们将上一条回调地狱的代码改写一下:
new Promise((resolve,reject) => {
setTimeout(() => {
console.log(1)
resolve()
},1000)
}).then((res) => {
setTimeout(() => {
console.log(2)
},2000)
}).then((res) => {
setTimeout(() => {
console.log(3)
},3000)
}).catch((err) => {
console.log(err)
})
Promise也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。
promise手写实现,面试够用版:
function myPromise(constructor){
let self=this;
self.status="pending" //定义状态改变前的初始状态
self.value=undefined;//定义状态为resolved的时候的状态
self.reason=undefined;//定义状态为rejected的时候的状态
function resolve(value){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
}
}
//捕获构造异常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
// 定义链式调用的then方法
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
58.Iterator是什么,有什么作用?
Iterator是理解第61条的先决知识,也许是我IQ不够😭,Iterator和Generator看了很多遍还是一知半解,即使当时理解了,过一阵又忘得一干二净。。。
Iterator(迭代器)是一种接口,也可以说是一种规范。为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator语法:
const obj = {
[Symbol.iterator]:function(){}
}
[Symbol.iterator] 属性名是固定的写法,只要拥有了该属性的对象,就能够用迭代器的方式进行遍历。
迭代器的遍历方法是首先获得一个迭代器的指针,初始时该指针指向第一条数据之前,接着通过调用 next 方法,改变指针的指向,让其指向下一条数据
每一次的 next 都会返回一个对象,该对象有两个属性
value 代表想要获取的数据
done 布尔值,false表示当前指针指向的数据有值,true表示遍历已经结束
Iterator 的作用有三个:
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
- 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
不断调用指针对象的next方法,直到它指向数据结构的结束位置。
每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
let arr = [{num:1},2,3]
let it = arr[Symbol.iterator]() // 获取数组中的迭代器
console.log(it.next()) // { value: Object { num: 1 }, done: false }
console.log(it.next()) // { value: 2, done: false }
console.log(it.next()) // { value: 3, done: false }
console.log(it.next()) // { value: undefined, done: true }
59.Generator函数是什么,有什么作用?
Generator函数可以说是Iterator接口的具体实现方式。Generator 最大的特点就是可以控制函数的执行。
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)
}
let it = foo(5)
console.log(it.next()) // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}
上面这个示例就是一个Generator函数,我们来分析其执行过程:
首先 Generator 函数调用时它会返回一个迭代器
当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42
Generator 函数一般见到的不多,其实也于他有点绕有关系,并且一般会配合 co 库去使用。当然,我们可以通过 Generator 函数解决回调地狱的问题。
60.什么是 async/await 及其如何工作,有什么优缺点?
async/await是一种建立在Promise之上的编写异步或非阻塞代码的新方法,被普遍认为是 JS异步操作的最终且最优雅的解决方案。相对于 Promise 和回调,它的可读性和简洁度都更高。毕竟一直then()也很烦。
async 是异步的意思,而 await 是 async wait的简写,即异步等待。
所以从语义上就很好理解 async 用于声明一个 function 是异步的,而await 用于等待一个异步方法执行完成。
一个函数如果加上 async ,那么该函数就会返回一个 Promise
async function test(){
return "1"
}
console.log(test())// -> Promise {<resolved>: "1"}
可以看到输出的是一个Promise对象。所以,async 函数返回的是一个 Promise 对象,如果在 async 函数中直接 return 一个直接量,async 会把这个直接量通过 PromIse.resolve() 封装成Promise对象返回。
相比于 Promise,async/await能更好地处理 then 链
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
现在分别用 Promise 和async/await来实现这三个步骤的处理。
使用Promise
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
});
}
doIt();
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
使用async/await
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
}
doIt();
结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,优雅整洁,几乎跟同步代码一样。
await关键字只能在async function中使用。在任何非async function的函数中使用await关键字都会抛出错误。await关键字在执行下一行代码之前等待右侧表达式(可能是一个Promise)返回。
优缺点:
async/await的优势在于处理 then 的调用链,能够更清晰准确的写出代码,并且也能优雅地解决回调地狱问题。
当然也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。
61.instanceof的原理是什么,如何实现?
instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。
实现 instanceof:
- 首先获取类型的原型
- 然后获得对象的原型
- 然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null
通过比较实例化对象a的__proto__
属性也就是a的属性是不是B的构造函数prototype
属性。
function myInstanceof(left, right) {
let prototype = right.prototype
left = left.__proto__
while (true) {
if (left === null || left === undefined)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
62.js 的节流与防抖
- 函数防抖
是指在事件触发n秒后再执行回调,如果在这n秒内事件又被触发,则重新计时;
这可以使用一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求
。 - 函数节流
是指规定一个单位时间,在这个单位事件内,只能有一次触发事件的回调函数执行
,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在scroll函数的事件监听上,通过事件节流来降低事件调用的频率;呃呃或者是缓存的expire上
// 函数防抖的实现
function debounce(fn, wait) {
var timer = null;
return function() {
var context = this,
args = arguments;
// 如果此时存在定时器的话,则取消之前的定时器重新记时
if (timer) {
clearTimeout(timer);
timer = null;
}
// 设置定时器,使事件间隔指定时间后执行
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
};
}
// 函数节流的实现;
function throttle(fn, delay) {
var preTime = Date.now();
return function() {
var context = this,
args = arguments,
nowTime = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。
if (nowTime - preTime >= delay) {
preTime = Date.now();
return fn.apply(context, args);
}
};
}
文章来源:FE- interview