1. JS 有哪些数据类型?
根据 JavaScript 中的变量类型传递方式,分为基本数据类型和引用数据类型两大类七种。
基本数据类型包括Undefined、Null、Boolean、Number、String、Symbol (ES6新增)六种.
引用数据类型只有Object一种,主要包括对象、数组和函数。判断数据类型采用typeof
操作符,有两种语法:
2. 基本数据类型和引用数据类型有什么区别?
(1)两者作为函数的参数进行传递时:
基本数据类型传入的是数据的副本,原数据的更改不会影响传入后的数据。
引用数据类型传入的是数据的引用地址,原数据的更改会影响传入后的数据。
(2)两者在内存中的存储位置:
基本数据类型存储在栈中。
引用数据类型在栈中存储了指针,该指针指向的数据实体存储在堆中。
3. 判断数据类型的方法有哪些?
(1)利用typeof
可以判断数据的类型;
(2)A instanceof
B可以用来判断A是否为B的实例,但它不能检测 null 和 undefined;
(3)B.constructor == A
可以判断A是否为B的原型,但constructor检测 Object与instanceof不一样,还可以处理基本数据类型的检测。
不过函数的 constructor 是不稳定的,这个主要体现在把类的原型进行重写,在重写的过程中很有可能出现把之前的constructor给覆盖了,这样检测出来的结果就是不准确的。
(4)Object.prototype.toString.call()
Object.prototype.toString.call()
是最准确最常用的方式。
4. 与深拷贝有何区别?如何实现?
浅拷贝只复制指向某个对象的指针,而不复制对象本身。浅拷贝的实现方式有:
(1)Object.assign()
:需注意的是目标对象只有一层的时候,是深拷贝;
(2)扩展运算符;深拷贝就是在拷贝数据的时候,将数据的所有引用结构都拷贝一份。深拷贝的实现方式有:
(1)手写遍历递归赋值;
(2)结合使用JSON.parse()
和JSON.stringify()
方法。
5. var let const的区别是什么?
var
、let
、const
都是用于声明变量或函数的关键字。其区别在于:
1. var: 传统的变量声明方式
在ES5及之前的JavaScript版本中,我们通常使用var关键字声明变量。var具有以下特点:
函数作用域:变量的作用域限制在声明的函数内部,如果在函数外部访问,将会报错。
变量提升:变量声明会被提升到作用域的顶部,无论声明语句在哪里,都会被视为在作用域的开始处声明的。
可以重复声明:同一个作用域内,可以多次使用var声明同一个变量,后面的声明会覆盖前面的。
虽然var在过去工作中表现良好,但它的作用域机制和变量提升的特性常常导致意外的bug。为了解决这些问题,ES6引入了let和const。
2. let: 块级作用域的变量声明方式
let关键字是在ES6中引入的新特性,具有以下特点:
块级作用域:使用let声明的变量仅在其所在的代码块内有效,如果在代码块外部访问,将会报错。这种特性有助于提高代码的可读性和可维护性。
不可重复声明:同一个作用域内,不可以多次使用let声明同一个变量,否则会报错。
没有变量提升:let声明的变量不会被提升到作用域的顶部,只有在声明语句之后才能使用。
让我们来看一个示例,以更好地理解let关键字的作用:
{ let x = 10; console.log(x); // 输出 10}console.log(x); // 报错,x 未定义
在上面的例子中,变量x只在大括号内部的代码块中有效,尝试在代码块外部访问会导致错误。这种行为使得我们可以更好地控制变量的作用范围,提高代码质量。
3. const: 声明常量的方式
const关键字也是在ES6中引入的新特性,与let相似,但具有以下特点:
常量:使用const声明的变量是常量,意味着一旦被赋值后,就不能再修改。常量的命名通常使用全大写字母,并采用下划线分隔单词。
块级作用域:与let一样,const也具有块级作用域。
不可重复声明:同一个作用域内,不可以多次使用const声明同一个变量,否则会报错。
常量的不可修改性可以帮助我们避免错误的赋值操作,提高代码的可靠性。以下是一个常量的示例:
const PI = 3.14;console.log(PI); // 输出 3.14PI = 3.1415; // 报错,常量不可被修改
4. 总结
通过对var、let和const关键字的介绍,我们可以看到它们在作用域和变量声明的特性上有所不同。var是传统的变量声明方式,具有函数作用域和变量提升的特点。let是ES6引入的新特性,具有块级作用域和不变提升的特点。const也是ES6引入的新特性,用于声明常量。
6. 什么是执行上下文和执行栈?
变量或函数的执行上下文,决定了它们的行为以及可以访问哪些数据。每个上下文都有一个关联的变量对象,而这个上下文中定义的所有变量和函数都存在于这个对象上(如DOM中全局上下文关联的便是window
对象)。
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文被推到一个执行栈中。在函数执行完之后,执行栈会弹出该函数上下文,在其上的所有变量和函数都会被销毁,并将控制权返还给之前的执行上下文。 JS的执行流就是通过这个执行栈进行控制的。
7. 什么是作用域和作用域链?
作用域可以理解为一个独立的地盘,可以理解为标识符所能生效的范围。作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。ES6中有全局作用域、函数作用域和块级作用域三层概念。
当一个变量在当前块级作用域中未被定义时,会向父级作用域(创建该函数的那个父级作用域)寻找。如果父级仍未找到,就会再一层一层向上寻找,直到找到全局作用域为止。这种一层一层的关系,就是作用域链
8. 作用域和执行上下文的区别是什么?
(1)函数的执行上下文只在函数被调用时生成,而其作用域在创建时已经生成;
(2)函数的作用域会包含若干个执行上下文(有可能是零个,当函数未被调用时)。
9. this指向的各种情况都有什么?
this的指向只有在调用时才能被确定,因为this
是执行上下文的一部分。
(1)全局作用域中的函数:其内部this
指向window
:
(2)对象内部的函数:其内部this
指向对象本身:
(3)构造函数:其内部this
指向生成的实例:
(4)由apply
、call
、bind
改造的函数:其this
指向第一个参数:
5)箭头函数:箭头函数没有自己的this
,看其外层的是否有函数,如果有,外层函数的this
就是内部箭头函数的this
,如果没有,则this
是window
。
10.如何改变this指针的指向?
可以使用apply
、call
、bind
方法改变this
指向(并不会改变函数的作用域)。比较如下:
(1)三者第一个参数都是this
要指向的对象,也就是想指定的上下文,上下文就是指调用函数的那个对象(没有就指向全局window);
(2)apply
的第二个参数是数组或者类数组对象,bind
和call
接收多个参数并用逗号隔开;
(3)apply
和call
只对原函数做改动,bind
会返回新的函数(要生效还得再调用一次)。
11.什么是闭包?
闭包就是引用了其他函数作用域中变量的函数,这种模式通常在函数嵌套结构中实现。里面的函数可以访问外面函数的变量,外面的变量的是这个内部函数的一部分。闭包有如下作用:
(1)加强封装,模拟实现私有变量;
(2)实现常驻内存的变量。
闭包不能滥用,否则会导致内存泄露,影响网页的性能。闭包使用完了后,要立即释放资源,将引用变量指向null。
12. 什么是原型、原型链?
原型:JS声明构造函数(用来实例化对象的函数)时,会在内存中创建一个对应的对象,这个对象就是原函数的原型。构造函数默认有一个prototype属性,prototype
的值指向函数的原型。同时原型中也有一个constructor
属性,constructor
的值指向原函数。
通过构造函数实例化出来的对象,并不具有prototype
属性,其默认有一个__proto__
属性,__proto__
的值指向构造函数的原型对象。在原型对象上添加或修改的属性,在所有实例化出的对象上都可共享。
当在实例化的对象中访问一个属性时,首先会在该对象内部寻找,如找不到,则会向其__proto__
指向的原型中寻找,如仍找不到,则继续向原型中__proto__
指向的上级原型中寻找,直至找到或Object.prototype
为止,这种链状过程即为原型链。
13. 何为防抖和节流?如何实现?
防抖和节流都是防止短时间内高频触发事件的方案。
防抖的原理是:如果一定时间内多次执行了某事件,则只执行其中的最后一次。
节流的原理是:要执行的事件每隔一段时间会被冷却,无法执行。
应用场景有:搜索框实时搜索,滚动改变相关的事件。
14. 如何理解同步和异步?
同步:按照代码书写顺序一一执行处理指令的一种模式,上一段代码执行完才能执行下一段代码。
异步:可以理解为一种并行处理的方式,不必等待一个程序执行完,可以执行其它的任务。
JS之所以需要异步的原因在于JS是单线程运行的。常用的异步场景有:定时器、ajax请求、事件绑定。
15. JS是如何实现异步的?
JS引擎是单线程的,但又能实现异步的原因在于事件循环和任务队列体系。
事件循环:
JS 会创建一个类似于 while (true)
的循环,每执行一次循环体的过程称之为 Tick
。每次 Tick
的过程就是查看是否有待处理事件,如果有则取出相关事件及回调函数放入执行栈中由主线程执行。待处理的事件会存储在一个任务队列中,也就是每次 Tick
会查看任务队列中是否有需要执行的任务。
任务队列:
异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如 onclick
, setTimeout
, ajax
处理的方式都不同,这些异步操作是由浏览器内核的 webcore
来执行的,浏览器内核包含3种 webAPI,分别是 DOM Binding
、network
、timer
模块。
onclick
由 DOM Binding
模块来处理,当事件触发的时候,回调函数会立即添加到任务队列中。 setTimeout
由 timer
模块来进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。 ajax
由network
模块来处理,在网络请求完成返回之后,才将回调添加到任务队列中。
主线程:
JS 只有一个线程,称之为主线程。而事件循环是主线程中执行栈里的代码执行完毕之后,才开始执行的。所以,主线程中要执行的代码时间过长,会阻塞事件循环的执行,也就会阻塞异步操作的执行。
只有当主线程中执行栈为空的时候(即同步代码执行完后),才会进行事件循环来观察要执行的事件回调,当事件循环检测到任务队列中有事件就取出相关回调放入执行栈中由主线程执行。
16. 什么是AJAX?如何实现?
ajax
是一种能够实现局部网页刷新的技术,可以使网页异步刷新。
ajax
的实现主要包括四个步骤:
(1)创建核心对象XMLhttpRequest
;
(2)利用open
方法打开与服务器的连接;
(3)利用send
方法发送请求;("POST"请求时,还需额外设置请求头)
(4)监听服务器响应,接收返回值。
17. 实现异步的方式有哪些?
(1)回调函数模式:将需要异步执行的函数作为回调函数执行,其缺点在于处理复杂逻辑异步逻辑时,会造成回调地狱(回调嵌套层数太多,代码结构混乱);
(2)事件监听模式:采用事件驱动的思想,当某一事件发生时触发执行异步函数,其缺点在于整个代码全部得变为事件驱动模式,难以分辨主流程;
(3)发布订阅模式:当异步任务执行完成时发布消息给信号中心,其他任务通过在信号中心中订阅消息来确定自己是否开始执行;
(4)Promise(ES6):Promise
对象共有三种状态pending
(初始化状态)、fulfilled
(成功状态)、rejected
(失败状态)。
(5)async/await(ES7):基于Promise
实现的异步函数; (6)利用生成器实现。
18. 怎么理解Promise对象?
Promise
对象有如下两个特点:
(1)对象的状态不受外界影响。Promise
对象共有三种状态pending
、fulfilled
、rejected
。状态值只会被异步结果决定,其他任何操作无法改变。
(2)状态一旦成型,就不会再变,且任何时候都可得到这个结果。状态值会由pending
变为fulfilled
或rejected
,这时即为resolved
。
Promise的缺点有如下三个缺点:
(1)Promise
一旦执行便无法被取消;
(2)不可设置回调函数,其内部发生的错误无法捕获;
(3)当处于pending
状态时,无法得知其具体发展到了哪个阶段。
Pomise
中常用的方法有:
(1)Promise.prototype.then()
:Promise
实例的状态发生改变时,会调用then
内部的回调函数。then
方法接受两个参数(第一个为resolved
状态时时执行的回调,第一个为rejected
状态时时执行的回调)
(2)Promise.prototype.catch()
:.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。
19. 怎么理解宏任务,微任务???
宏任务有:script(整体代码)
、setTimeout
、setInterval
、I/O
、页面渲染;
微任务有:Promise.then
、Object.observe
、MutationObserver
。
执行顺序大致如下:
主线程任务——>宏任务——>微任务——>微任务里的宏任务——>.......——>直到任务全部完成
20. 实现继承的方法有哪些???
实现继承的方法有:
(1)class+extends继承(ES6)
(2)原型继承
(3)借用构造函数继承
(4)寄生组合式继承(重点)
可以看到很好的继承了父类的方法和属性以及自己添加属性和方法,并且只调用了1次父类构造函数,同时保证了原型链的完整,是一种理想的继承方法。下图是自己理解的图示,如果有错误希望大佬来指正