由浅入深详细整理JavaScript面试知识点
本篇文章给大家带来了关于javascript面试知识点的详细总结,文章总结了66条JavaScript知识点由浅入深,希望对大家有帮助。
前言
我只想面个CV工程师,面试官偏偏让我挑战造火箭工程师,加上今年这个情况更是前后两男
,但再难苟且的生活还要继续,饭碗还是要继续找的。在最近的面试中我一直在总结,每次面试回来也都会复盘,下面是我这几天遇到的面试知识点。但今天主题是标题所写的66条JavaScript知识点,由浅入深,整理了一周,每(zhěng)天(lǐ)整(bù)理( yì)10条( qiú)左(diǎn)右(zàn), 希望对正在找工作的小伙伴有点帮助,文中如有表述不对,还请指出。
HTML&CSS:
- 浏览器内核
- 盒模型、flex布局、两/三栏布局、水平/垂直居中;
- BFC、清除浮动;
- css3动画、H5新特性。
JavaScript:
- 继承、原型链、this指向、设计模式、call, apply, bind,;
- new实现、防抖节流、let, var, const 区别、暂时性死区、event、loop;
- promise使用及实现、promise并行执行和顺序执行;
- async/await的优缺点;
- 闭包、垃圾回收和内存泄漏、数组方法、数组乱序, 数组扁平化、事件委托、事件监听、事件模型
Vue:
- vue数据双向绑定原理;
- vue computed原理、computed和watch的区别;
- vue编译器结构图、生命周期、vue组件通信;
- mvvm模式、mvc模式理解;
- vue dom diff、vuex、vue-router
网络:
- HTTP1, HTTP2, HTTPS、常见的http状态码;
- 浏览从输入网址到回车发生了什么;
- 前端安全(CSRF、XSS)
- 前端跨域、浏览器缓存、cookie, session, token, localstorage, sessionstorage;
- TCP连接(三次握手, 四次挥手)
性能相关
- 图片优化的方式
- 500 张图片,如何实现预加载优化
- 懒加载具体实现
- 减少http请求的方式
- webpack如何配置大型项目
另外更全面的面试题集我也在整理中,先给个预告图:
下面进入正题:
1.介绍一下js的数据类型有哪些,值是如何存储的
具体可看我之前的文章:「前端料包」可能是最透彻的JavaScript数据类型详解
JavaScript一共有8种数据类型,其中有7种基本数据类型:Undefined、Null、Boolean、Number、String、Symbol(es6新增,表示独一无二的值)和BigInt(es10新增);
1种引用数据类型——Object(Object本质上是由一组无序的名值对组成的)。里面包含 function、Array、Date等。JavaScript不支持任何创建自定义类型的机制,而所有值最终都将是上述 8 种数据类型之一。
原始数据类型:直接存储在栈(stack)中,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。
引用数据类型:同时存储在栈(stack)和堆(heap)中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
2. && 、 ||和!! 运算符分别能做什么
&&
叫逻辑与,在其操作数中找到第一个虚值表达式并返回它,如果没有找到任何虚值表达式,则返回最后一个真值表达式。它采用短路来防止不必要的工作。||
叫逻辑或,在其操作数中找到第一个真值表达式并返回它。这也使用了短路来防止不必要的工作。在支持 ES6 默认函数参数之前,它用于初始化函数中的默认参数值。!!
运算符可以将右侧的值强制转换为布尔值,这也是将值转换为布尔值的一种简单方法。
3. js的数据类型的转换
在 JS 中类型转换只有三种情况,分别是:
- 转换为布尔值(调用Boolean()方法)
- 转换为数字(调用Number()、parseInt()和parseFloat()方法)
- 转换为字符串(调用.toString()或者String()方法)
null和underfined没有.toString方法
此外还有一些操作符会存在隐式转换,此处不做展开,可自行百度00
4. JS中数据类型的判断( typeof,instanceof,constructor,Object.prototype.toString.call()
(1)typeof
typeof 对于原始类型来说,除了 null 都可以显示正确的类型
console.log(typeof 2); // numberconsole.log(typeof true); // booleanconsole.log(typeof 'str'); // stringconsole.log(typeof []); // object []数组的数据类型在 typeof 中被解释为 objectconsole.log(typeof function(){}); // functionconsole.log(typeof {}); // objectconsole.log(typeof undefined); // undefinedconsole.log(typeof null); // object null 的数据类型被 typeof 解释为 object
typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型,所以想判断一个对象的正确类型,这时候可以考虑使用 instanceof
(2)instanceof
instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。
console.log(2 instanceof Number); // falseconsole.log(true instanceof Boolean); // false console.log('str' instanceof String); // false console.log([] instanceof Array); // trueconsole.log(function(){} instanceof Function); // trueconsole.log({} instanceof Object); // true // console.log(undefined instanceof Undefined); // console.log(null instanceof Null);
可以看出直接的字面量值判断数据类型,instanceof可以精准判断引用数据类型(Array,Function,Object),而基本数据类型不能被instanceof精准判断。
我们来看一下 instanceof 在MDN中的解释:instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。其意思就是判断对象是否是某一数据类型(如Array)的实例,请重点关注一下是判断一个对象是否是数据类型的实例。在这里字面量值,2, true ,'str’不是实例,所以判断值为false。
(3)constructor
console.log((2).constructor === Number); // trueconsole.log((true).constructor === Boolean); // trueconsole.log(('str').constructor === String); // trueconsole.log(([]).constructor === Array); // trueconsole.log((function() {}).constructor === Function); // trueconsole.log(({}).constructor === Object); // true复制代码 这里有一个坑,如果我创建一个对象,更改它的原型,constructor就会变得不可靠了 复制代码function Fn(){}; Fn.prototype=new Array(); var f=new Fn(); console.log(f.constructor===Fn); // falseconsole.log(f.constructor===Array); // true
(4)Object.prototype.toString.call() 使用 Object 对象的原型方法 toString ,使用 call 进行狸猫换太子,借用Object的 toString 方法
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));
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
详细资料可以参考: 《标准内置对象的分类》
《JS 所有内置对象属性和方法汇总》
6. undefined 与 undeclared 的区别?
已在作用域中声明但还没有赋值的变量,是 undefined。相反,还没有在作用域中声明过的变量,是 undeclared 的。
对于 undeclared 变量的引用,浏览器会报引用错误,如 ReferenceError: b is not defined 。但是我们可以使用 typ eof 的安全防范机制来避免报错,因为对于 undeclared(或者 not defined )变量,typeof 会返回 “undefined”。
7. null 和 undefined 的区别?
首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
undefined 代表的含义是未定义, null 代表的含义是空对象(其实不是真的对象,请看下面的注意!)。一般变量声明了但还没有定义的时候会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。
其实 null 不是对象,虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
undefined 在 js 中不是一个保留字,这意味着我们可以使用 undefined 来作为一个变量名,这样的做法是非常危险的,它 会影响我们对 undefined 值的判断。但是我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。
当我们对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 “object”,这是一个历史遗留的问题。当我们使用双等 号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
详细资料可以参考:
《JavaScript 深入理解之 undefined 与 null》
8. {}和[]的valueOf和toString的结果是什么?
{} 的 valueOf 结果为 {} ,toString 的结果为 "[object Object]"[] 的 valueOf 结果为 [] ,toString 的结果为 ""
9. Javascript 的作用域和作用域链
作用域: 作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找。
作用域链: 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和 函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前 端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找。
作用域链的创建过程跟执行上下文的建立有关…
详细资料可以参考: 《JavaScript 深入理解之作用域链》
也可以看看我的文章:「前端料包」深究JavaScript作用域(链)知识点和闭包
10. javascript 创建对象的几种方式?
我们一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。但 js和一般的面向对象的语言不同,在 ES6 之前它没有类的概念。但是我们可以使用函数来进行模拟,从而产生出可复用的对象创建方式,我了解到的方式有这么几种:
- 第一种是工厂模式,工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。
- 第二种是构造函数模式。js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么我们就可以把它称为构造函数。执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,因此我们可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此我们可以通过原型来识别对象的类型。但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次我们都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。
- 第三种模式是原型模式,因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此我们可以使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来说,解决了函数对象的复用问题。但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。
- 第四种模式是组合使用构造函数模式和原型模式,这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存在一些问题,因此我们可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用。这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使用了两种不同的模式,所以对于代码的封装性不够好。
- 第五种模式是动态原型模式,这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。
- 第六种模式是寄生构造函数模式,这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。
嗯我目前了解到的就是这么几种方式。
详细资料可以参考: 《JavaScript 深入理解之对象创建》
11. JavaScript 继承的几种实现方式?
我了解的 js 中实现继承的几种方式有:
- 第一种是以原型链的方式来实现继承,但是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传递参数。
- 第二种方式是使用借用构造函数的方式,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的,这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。
- 第三种方式是组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。
- 第四种方式是原型式继承,原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。
- 第五种方式是寄生式继承,寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是我们的自定义类型时。缺点是没有办法实现函数的复用
- 第六种方式是寄生式组合继承,组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。(详细解释见红宝书247-248页)
复制代码
详细资料可以参考: 《JavaScript 深入理解之继承》
12. 寄生式组合继承的实现?
function Person(name) { this.name = name;}Person.prototype.sayName = function() { console.log("My name is " + this.name + ".");};function Student(name, grade) { Person.call(this, name); this.grade = grade;}Student.prototype = Object.create(Person.prototype);Student.prototype.constructor = Student;Student.prototype.sayMyGrade = function() { console.log("My grade is " + this.grade + "."); };
13. 谈谈你对this、call、apply和bind的理解
详情可看我之前的文章:「前端料包」一文彻底搞懂JavaScript中的this、call、apply和bind
- 在浏览器里,在全局范围内this 指向window对象;
- 在函数中,this永远指向最后调用他的那个对象;
- 构造函数中,this指向new出来的那个新的对象;
- call、apply、bind中的this被强绑定在指定的那个对象上;
- 箭头函数中this比较特殊,箭头函数this为父作用域的this,不是调用时的this.要知道前四种方式,都是调用时确定,也就是动态的,而箭头函数的this指向是静态的,声明的时候就确定了下来;
- apply、call、bind都是js给函数内置的一些API,调用他们可以为函数指定this的执行,同时也可以传参。
14. JavaScript 原型,原型链? 有什么特点?
在 js 中我们是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对 象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当我们使用构造函数新建一个对象后,在这个对象的内部 将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说我们 是不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来让我们访问这个属性,但是我们最好不要使用这 个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,我们可以通过这个方法来获取对 象的原型。
当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又 会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就 是我们新建的对象为什么能够使用 toString() 等方法的原因。
特点:
JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与 之相关的对象也会继承这一改变。
参考文章:
《JavaScript 深入理解之原型与原型链》
也可以看看我写的:「前端料包」深入理解JavaScript原型和原型链
15. js 获取原型的方法?
- p.proto
- p.constructor.prototype
- Object.getPrototypeOf§
16. 什么是闭包,为什么要用它?
闭包是指有权访问另一个函数作用域内变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以 访问到当前函数的局部变量。
闭包有两个常用的用途。
- 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
function a(){ var n = 0; function add(){ n++; console.log(n); } return add;}var a1 = a(); //注意,函数名只是一个标识(指向函数的指针),而()才是执行函数;a1(); //1a1(); //2 第二次调用n变量还在内存中
其实闭包的本质就是作用域链的一个特殊的应用,只要了解了作用域链的创建过程,就能够理解闭包的实现原理。
17. 什么是 DOM 和 BOM?
DOM 指的是文档对象模型,它指的是把文档当做一个对象来对待,这个对象主要定义了处理网页内容的方法和接口。
BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。BOM 的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局) 对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 locati on 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对 象的子对象。
相关资料:
《DOM, DOCUMENT, BOM, WINDOW 有什么区别?》
《Window 对象》
《DOM 与 BOM 分别是什么,有何关联?》
《JavaScript 学习总结(三)BOM 和 DOM 详解》
18. 三种事件模型是什么?
事件 是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型。
- DOM0级模型: ,这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过 js属性来指定监听函数。这种方式是所有浏览器都兼容的。
- IE 事件模型: 在该事件模型中,一次事件共有两个过程,事件处理阶段,和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过 attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。
- DOM2 级事件模型: 在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。
相关资料:
《一个 DOM 元素绑定多个事件时,先执行冒泡还是捕获》
19. 事件委托是什么?
事件委托 本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到 目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理。
使用事件代理我们可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理我们还可以实现事件的动态绑定,比如说新增了一个子节点,我们并不需要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。
相关资料:
《JavaScript 事件委托详解》
20. 什么是事件传播?
当事件发生在DOM元素上时,该事件并不完全发生在那个元素上。在“当事件发生在DOM元素上时,该事件并不完全发生在那个元素上。
事件传播有三个阶段:
- 捕获阶段–事件从 window 开始,然后向下到每个元素,直到到达目标元素事件或event.target。
- 目标阶段–事件已达到目标元素。
- 冒泡阶段–事件从目标元素冒泡,然后上升到每个元素,直到到达 window。
21. 什么是事件捕获?
当事件发生在 DOM 元素上时,该事件并不完全发生在那个元素上。在捕获阶段,事件从window开始,一直到触发事件的元素。window----> document----> html----> body ---->目标元素
假设有如下的 HTML 结构:
<p class="grandparent"> <p class="parent"> <p class="child">1</p> </p> </p>
对应的 JS 代码:
function addEvent(el, event, callback, isCapture = false) { if (!el || !event || !callback || typeof callback !== 'function') return; if (typeof el === 'string') { el = document.querySelector(el); }; el.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
,这就是事件捕获。
22. 什么是事件冒泡?
事件冒泡刚好与事件捕获相反,当前元素---->body ----> html---->document ---->window
。当事件发生在DOM元素上时,该事件并不完全发生在那个元素上。在冒泡阶段,事件冒泡,或者事件发生在它的父代,祖父母,祖父母的父代,直到到达window为止。
假设有如下的 HTML 结构:
<p class="grandparent"> <p class="parent"> <p class="child">1</p> </p></p>
对应的JS代码:
function addEvent(el, event, callback, isCapture = false) { if (!el || !event || !callback || typeof callback !== 'function') return; if (typeof el === 'string') { el = document.querySelector(el); }; el.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
元素,它将分别在控制台上打印child
,parent
,grandparent
,html
,document
和window
,这就是事件冒泡。
23. DOM 操作——怎样添加、移除、移动、复制、创建和查找节点?
(1)创建新节点
createDocumentFragment() //创建一个DOM片段 createElement() //创建一个具体的元素 createTextNode() //创建一个文本节点
(2)添加、移除、替换、插入
appendChild(node)removeChild(node)replaceChild(new,old)insertBefore(new,old)
(3)查找
getElementById();getElementsByName(); getElementsByTagName(); getElementsByClassName();querySelector(); querySelectorAll();
(4)属性操作
getAttribute(key);setAttribute(key, value); hasAttribute(key);removeAttribute(key);
相关资料:
《DOM 概述》
《原生 JavaScript 的 DOM 操作汇总》
《原生 JS 中 DOM 节点相关 API 合集》
24. js数组和字符串有哪些原生方法,列举一下
25. 常用的正则表达式(仅做收集,涉及不深)
//(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}$/
26. Ajax 是什么? 如何创建一个 Ajax?
我对 ajax 的理解是,它是一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。
创建步骤:
面试手写(原生):
//1:创建Ajax对象var xhr = window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');// 兼容IE6及以下版本//2:配置 Ajax请求地址xhr.open('get','index.xml',true);//3:发送请求xhr.send(null); // 严谨写法//4:监听请求,接受响应xhr.onreadysatechange=function(){ if(xhr.readySate==4&&xhr.status==200 || xhr.status==304 ) console.log(xhr.responsetXML)
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;}
27. js 延迟加载的方式有哪些?
js 的加载、解析和执行会阻塞页面的渲染过程,因此我们希望 js 脚本能够尽可能的延迟加载,提高页面的渲染速度。
我了解到的几种方式是:
- 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。
- 给 js 脚本添加 defer属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
- 给 js 脚本添加 async属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
- 动态创建 DOM 标签的方式,我们可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
相关资料:
《JS 延迟加载的几种方式》
《HTML 5 `` async
属性》
28. 谈谈你对模块化开发的理解?
我对模块的理解是,一个模块是实现一个特定功能的一组方法。在最开始的时候,js 只实现一些简单的功能,所以并没有模块的概念 ,但随着程序越来越复杂,代码的模块化开发变得越来越重要。
由于函数具有独立作用域的特点,最原始的写法是使用函数来作为模块,几个函数作为一个模块,但是这种方式容易造成全局变量的污 染,并且模块间没有联系。
后面提出了对象写法,通过将函数作为一个对象的方法来实现,这样解决了直接使用函数作为模块的一些缺点,但是这种办法会暴露所 有的所有的模块成员,外部代码可以修改内部属性的值。
现在最常用的是立即执行函数的写法,通过利用闭包来实现模块私有作用域的建立,同时不会对全局作用域造成污染。
相关资料: 《浅谈模块化开发》
《Javascript 模块化编程(一):模块的写法》
《前端模块化:CommonJS,AMD,CMD,ES6》
《Module 的语法》
29. js 的几种模块规范?
js 中现在比较成熟的有四种模块加载方案:
- 第一种是 CommonJS 方案,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
- 第二种是 AMD 方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。
- 第三种是 CMD 方案,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。它和require.js的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
- 第四种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块。
30. AMD 和 CMD 规范的区别?
它们之间的主要区别有两个方面。
- 第一个方面是在模块定义时对依赖的处理不同。AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块。而 CMD 推崇就近依赖,只有在用到某个模块的时候再去 require。
- 第二个方面是对依赖模块的执行时机处理不同。首先 AMD 和 CMD 对于模块的加载方式都是异步加载,不过它们的区别在于
模块的执行时机,AMD 在依赖模块加载完成后就直接执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致。而 CMD 在依赖模块加载完成后并不执行,只是下载而已,等到所有的依赖模块都加载好后,进入回调函数逻辑,遇到 require 语句 的时候才执行对应的模块,这样模块的执行顺序就和我们书写的顺序保持一致了。
// CMDdefine(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(); // ...});
相关资料:
《前端模块化,AMD 与 CMD 的区别》
31. ES6 模块与 CommonJS 模块、AMD、CMD 的差异。
- 1.
CommonJS
模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。CommonJS
模块输出的是值的
,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
- 2.
CommonJS
模块是运行时加载,ES6 模块是编译时输出接口。CommonJS
模块就是对象,即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
32. requireJS的核心原理是什么?
require.js 的核心原理是通过动态创建 script 脚本来异步引入模块,然后对每个脚本的 load 事件进行监听,如果每个脚本都加载完成了,再调用回调函数。"
详细资料可以参考: 《requireJS 的用法和原理分析》
《requireJS 的核心原理是什么?》
《requireJS 原理分析》
33. 谈谈JS的运行机制
1. js单线程
JavaScript语言的一大特点就是单线程,即同一时间只能做一件事情。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
2. js事件循环
js代码执行过程中会有很多任务,这些任务总的分成两类:
- 同步任务
- 异步任务
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。,我们用导图来说明: 我们解释一下这张图:
- 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入
Event Table
并注册函数。 - 当指定的事情完成时,
Event Table
会将这个函数移入Event Queue
。 - 主线程内的任务执行完毕为空,会去
Event Queue
读取对应的函数,进入主线程执行。 - 上述过程会不断重复,也就是常说的
Event Loop
(事件循环)。
那主线程执行栈何时为空呢?js引擎存在monitoring process
进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue
那里检查是否有等待被调用的函数。
以上就是js运行的整体流程
需要注意的是除了同步任务和异步任务,任务还可以更加细分为macrotask(宏任务)和microtask(微任务),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,将setTimeout的回调函数丢到宏任务队列中,在往下执行new Promise立即执行,输出2,then的回调函数丢到微任务队列中,再继续执行,遇到process.nextTick,同样将回调函数扔到为任务队列,再继续执行,输出5,当所有同步任务执行完成后看有没有可以执行的微任务,发现有then函数和nextTick两个微任务,先执行哪个呢?process.nextTick指定的异步任务总是发生在所有异步任务之前,因此先执行process.nextTick输出4然后执行then函数输出3,第一轮执行结束。 第二轮:从宏任务队列开始,发现setTimeout回调,输出1执行完毕,因此结果是25431
相关资料:
《浏览器事件循环机制(event loop)》
《详解 JavaScript 中的 Event Loop(事件循环)机制》
《什么是 Event Loop?》
《这一次,彻底弄懂 JavaScript 执行机制》
34. arguments 的对象是什么?
arguments对象是函数中传递的参数值的集合。它是一个类似数组的对象,因为它有一个length属性,我们可以使用数组索引表示法arguments[1]来访问单个值,但它没有数组中的内置方法,如:forEach、reduce、filter和map。
我们可以使用Array.prototype.slice将arguments对象转换成一个数组。
function one() { return 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
当我们调用函数four时,它会抛出一个ReferenceError: arguments is not defined error
。使用rest语法,可以解决这个问题。
const four = (...args) => args;
这会自动将所有参数值放入数组中。
35. 为什么在调用这个函数时,代码中的b
会变成一个全局变量?
function myFunc() { let a = b = 0;}myFunc();
原因是赋值运算符是从右到左的求值的。这意味着当多个赋值运算符出现在一个表达式中时,它们是从右向左求值的。所以上面代码变成了这样:
function myFunc() { let a = (b = 0);}myFunc();
首先,表达式b = 0求值,在本例中b没有声明。因此,JS引擎在这个函数外创建了一个全局变量b,之后表达式b = 0的返回值为0,并赋给新的局部变量a。
我们可以通过在赋值之前先声明变量来解决这个问题。
function myFunc() { let a,b; a = b = 0;}myFunc();
36. 简单介绍一下 V8 引擎的垃圾回收机制
v8 的垃圾回收机制基于分代回收机制,这个机制又基于世代假说,这个假说有两个特点,一是新生的对象容易早死,另一个是不死的对象会活得更久。基于这个假说,v8 引擎将内存分为了新生代和老生代。 新创建的对象或者只经历过一次的垃圾回收的对象被称为新生代。经历过多次垃圾回收的对象被称为老生代。 新生代被分为 From 和 To 两个空间,To 一般是闲置的。当 From 空间满了的时候会执行 Scavenge 算法进行垃圾回收。当我们执行垃圾回收算法的时候应用逻辑将会停止,等垃圾回收结束后再继续执行。这个算法分为三步: (1)首先检查 From 空间的存活对象,如果对象存活则判断对象是否满足晋升到老生代的条件,如果满足条件则晋升到老生代。如果不满足条件则移动 To 空间。 (2)如果对象不存活,则释放对象的空间。 (3)最后将 From 空间和 To 空间角色进行交换。 新生代对象晋升到老生代有两个条件: (1)第一个是判断是对象否已经经过一次 Scavenge 回收。若经历过,则将对象从 From 空间复制到老生代中;若没有经历,则复制到 To 空间。 (2)第二个是 To 空间的内存使用占比是否超过限制。当对象从 From 空间复制到 To 空间时,若 To 空间使用超过 25%,则对象直接晋升到老生代中。设置 25% 的原因主要是因为算法结束后,两个空间结束后会交换位置,如果 To 空间的内存太小,会影响后续的内存分配。 老生代采用了标记清除法和标记压缩法。标记清除法首先会对内存中存活的对象进行标记,标记结束后清除掉那些没有标记的对象。由于标记清除后会造成很多的内存碎片,不便于后面的内存分配。所以了解决内存碎片的问题引入了标记压缩法。 由于在进行垃圾回收的时候会暂停应用的逻辑,对于新生代方法由于内存小,每次停顿的时间不会太长,但对于老生代来说每次垃圾回收的时间长,停顿会造成很大的影响。 为了解决这个问题 V8 引入了增量标记的方法,将一次停顿进行的过程分为了多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行。
37. 哪些操作会造成内存泄漏?
1.意外的全局变量
2.被遗忘的计时器或回调函数
3.脱离 DOM 的引用
4.闭包
第一种情况是我们由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
第二种情况是我们设置了
setInterval
定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。第三种情况是我们获取一个DOM元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。
第四种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。
38. 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好没有尊严,为啥要搞个人出来约束自己,
那个人被创造出来也好委屈,自己被创造出来完全是因为要约束JavaScript。
39. ECMAScript 2015(ES6)有哪些新特性?
- 块作用域
- 类
- 箭头函数
- 模板字符串
- 加强的对象字面
- 对象解构
- Promise
- 模块
- Symbol
- 代理(proxy)Set
- 函数默认参数
- rest 和展开
40. var
,let
和const
的区别是什么?
var声明的变量会挂载在window上,而let和const声明的变量不会:
var a = 100;console.log(a,window.a); // 100 100let b = 10;console.log(b,window.b); // 10 undefinedconst c = 1;console.log(c,window.c); // 1 undefined
var声明变量存在变量提升,let和const不存在变量提升:
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声明形成块作用域
if(1){ var a = 100; let b = 10;}console.log(a); // 100console.log(b) // 报错:b is not defined ===> 找不到b这个变量-------------------------------------------------------------if(1){ var a = 100; const c = 1;}console.log(a); // 100console.log(c) // 报错:c is not defined ===> 找不到c这个变量
同一作用域下let和const不能声明同名变量,而var可以
var a = 100;console.log(a); // 100var 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'}
41. 什么是箭头函数?
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target
。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
//ES5 Versionvar getCurrentDate = function (){ return new Date();} //ES6 Versionconst getCurrentDate = () => new Date();
在本例中,ES5 版本中有function(){}
声明和return关键字,这两个关键字分别是创建函数和返回值所需要的。在箭头函数版本中,我们只需要()括号,不需要 return 语句,因为如果我们只有一个表达式或值需要返回,箭头函数就会有一个隐式的返回。
//ES5 Versionfunction greet(name) { return 'Hello ' + name + '!';} //ES6 Versionconst greet = (name) => `Hello ${name}`; const greet2 = name => `Hello ${name}`;
我们还可以在箭头函数中使用与函数表达式和函数声明相同的参数。如果我们在一个箭头函数中有一个参数,则可以省略括号。
const getArgs = () => argumentsconst getArgs2 = (...rest) => rest
箭头函数不能访问arguments对象。所以调用第一个getArgs函数会抛出一个错误。相反,我们可以使用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 对象。
42. 什么是类?
类(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 VersionEmployee.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 Versionclass 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); // trueconsole.log(s.toString === Object.prototype.toString); // true
43. 什么是模板字符串?
模板字符串是在 JS 中创建字符串的一种新方法。我们可以通过使用反引号使模板字符串化。
//ES5 Versionvar greet = 'Hi I\'m Mark'; //ES6 Versionlet greet = `Hi I'm Mark`;
在 ES5 中我们需要使用一些转义字符来达到多行的效果,在模板字符串不需要这么麻烦:
//ES5 Versionvar lastWords = '\n' + ' I \n' + ' Am \n' + 'Iron Man \n';//ES6 Versionlet lastWords = ` I Am Iron Man `;
在ES5版本中,我们需要添加\n以在字符串中添加新行。在模板字符串中,我们不需要这样做。
//ES5 Versionfunction greet(name) { return 'Hello ' + name + '!';}//ES6 Versionfunction greet(name) { return `Hello ${name} !`;}
在 ES5 版本中,如果需要在字符串中添加表达式或值,则需要使用+
运算符。在模板字符串s中,我们可以使用${expr}
嵌入一个表达式,这使其比 ES5 版本更整洁。
44. 什么是对象解构?
对象析构是从对象或数组中获取或提取值的一种新的、更简洁的方法。假设有如下的对象:
const employee = { firstName: "Marko", lastName: "Polo", position: "Software Developer", yearHired: 2017};
从对象获取属性,早期方法是创建一个与对象属性同名的变量。这种方法很麻烦,因为我们要为每个属性创建一个新变量。假设我们有一个大对象,它有很多属性和方法,用这种方法提取属性会很麻烦。
var firstName = employee.firstName; var lastName = employee.lastName; var position = employee.position; var yearHired = employee.yearHired;
使用解构方