176、Js中同步任务和异步任务的区别
同步任务
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。在执行同步任务时,主线程会一直被占用,直到任务执行完毕,因此同步任务也被称为阻塞式任务。
异步任务
异步任务指的是,不进入主线程、而进入任务队列(task queue)的任务,只有在对应的异步操作完成时,异步任务才会被放回主线程。在执行异步任务时,主线程并不会被阻塞,可以继续执行其他任务,因此异步任务也被称为非阻塞式任务。
异步任务通常会使用回调函数来进行处理,即在异步任务完成后,通过回调函数告知主线程。常见的异步操作包括定时器、Ajax 请求、Promise 等等。
同步代码的优点:
1.可读性强:同步代码的执行顺序是按照我们书写的顺序进行的,代码逻辑清晰,易于阅读和维护。
2.容易调试:同步代码的执行顺序固定,出现错误时容易找到问题所在。
同步代码的缺点:
1.效率低:同步代码的执行速度慢,如果某个操作需要较长时间,整个程序就会被阻塞,影响程序的响应速度和效率。
2.不灵活:同步代码的流程比较固定,对于一些需要根据不同情况进行处理的操作,同步代码可能无法胜任。
异步代码的优点:
1.效率高:异步代码的执行速度快,并且不会阻塞整个程序的运行。
2.灵活性强:异步代码的处理方式更加灵活,可以根据不同的情况进行不同的处理。
异步代码的缺点:
1.可读性差:异步代码的执行结果不是按照书写顺序而来的,这增加了代码的理解和维护难度。
2.调试困难:异步代码的执行过程较为复杂,出现错误时很难确定错误的来源。
总之,同步代码适合处理简单逻辑、执行时间短的任务,而异步代码适合处理复杂任务、执行时间长的任务。在实际开发当中,可以根据具体情况选择同步或者异步方式来编写代码。
177、Js中的单线程与事件循环
- Js是单线程,但是浏览器是多线程。
- Js中采用了事件循环(Event Loop)来执行异步任务。
- 所以,事件循环是一种异步编程模型,事件循环会不断地从任务队列(Task Queue)中取出待处理的任务并执行,直到任务队列为空为止。任务可以分为两类:宏任务(Macro Task)和微任务(Micro Task)。
- 微任务会优先于宏任务执行
在异步任务中又分 宏任务(macro-task)和微任务(micro-task)。 宏任务是由宿主(浏览器、node)发起的,微任务是由 js 引擎发起的。
宏任务大概包括
- script(整体代码)
- setTimeout
- setInterval
- setImmediate
- I/O
- UI render
微任务大概包括
- process.nextTick
- Promise
- Async/Await(实际就是promise)
- MutationObserver(html5新特性)
宏任务和微任务的执行过程
-
先执行同步代码
-
后执行微任务的异步代码
-
再执行宏任务的异步代码
setTimeout(() => { console.log(1); });
Promise.resolve().then(() => { console.log(2); }); console.log(3);
代码最终执行: 3 2 1
178、Js中的异步机制
JavaScript 中的异步机制可以让程序在执行一些比较耗时的操作(如网络请求、文件读写等)时,不必阻塞后续代码的执行,提高了程序的效率和用户体验。
以下是 JavaScript 中常见的异步机制:
- 回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
- Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
- async 函数 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。
- generator 的方式,它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来。因此在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式需要考虑的问题是何时将函数的控制权转移回来,因此需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。
- 事件监听与发布/订阅模式:
- 通过事件监听机制或者发布/订阅模式实现异步编程。例如,使用
addEventListener监听事件,或者使用第三方库如 EventEmitter 实现发布/订阅模式。 - 通过发布/订阅模式,不同模块之间可以进行解耦,实现更灵活的异步消息传递。
- 通过事件监听机制或者发布/订阅模式实现异步编程。例如,使用
179、script标签中defer和async的区别
<script> 标签中的 defer 属性和 async 属性都可以用来异步加载脚本文件,但它们有以下区别:
defer属性会让脚本在页面解析完成后再执行,即使脚本在defer属性之前加载完成,也会等到页面解析完成后才执行。多个带有defer属性的脚本文件的执行顺序是按照它们在页面中出现的顺序依次执行的。
例如:<script src="demo.js" defer></script>
async属性会让脚本异步加载并且不保证脚本的执行顺序,即使脚本在其它资源(如样式表,图片等)加载之前加载完成,也会立即执行。多个带有async属性的脚本文件的执行顺序也不能得到保证。
例如:<script src="demo.js" async></script>
技术详解
在前端开发中,async 和 defer 是用来控制 JavaScript 脚本加载和执行的两个属性,它们可以被用于 <script> 标签中。这两个属性的主要不同点在于脚本的加载和执行时机:
-
async属性:- 使用
async属性的脚本会异步加载,也就是说它会在 HTML 解析过程中同时下载脚本文件,但是不会阻塞 HTML 解析。 - 一旦脚本下载完成,它会立即执行,不管 HTML 解析是否完成。
- 多个带有
async属性的脚本之间的执行顺序没有固定规律,取决于各自加载完成的时间。
- 使用
-
defer属性:- 使用
defer属性的脚本也会异步加载,但是它会在 HTML 解析完成后才依次执行,而且在DOMContentLoaded事件触发之前执行。 - 多个带有
defer属性的脚本按照它们在文档中出现的顺序依次执行,保证了执行顺序的一致性。 defer属性适合用于需要按顺序执行的脚本,或者需要等待整个文档解析完成后再执行的情况。
- 使用
综上所述,async 和 defer 的主要区别在于脚本的加载和执行时机,async 脚本会在下载完成后立即执行,而 defer 脚本会在 HTML 解析完成后依次执行。根据实际需求和脚本的依赖关系,选择合适的属性可以优化页面加载性能和脚本执行顺序。
180、Js中的XMLHTTPRequest对象
XMLHttpRequest (XHR)对象是JavaScript的一个内置对象,是执行Ajax编程的重要组成部分,用于在Web应用程序中向服务器发起HTTP请求以获取或提交数据。
下面简要介绍一下XMLHTTPRequest对象及其回调方法:
XMLHttpRequest对象
XMLHttpRequest对象提供了许多属性和方法,可以使用它们操作HTTP请求和响应。其中最常用的包括:
open(): 初始化一个请求,即指定要进行的HTTP请求方法、资源URL和是否异步处理请求等。send(): 发送请求到服务器。对于 GET 请求,它需要发送null作为参数;对于 POST 请求,它需要将数据包含在函数调用中。setRequestHeader(): 设置HTTP请求头,包括Content-Type、Authorization等。getResponseHeader(): 获取已经收到的响应头。getAllResponseHeaders(): 获取所有的已经收到的响应头。
回调方法
由于JS是单线程执行的语言,如果没有回调函数,那么XMLHttpRequest必须等待服务器的响应之后才能执行后续的代码。而通过回调方法,我们可以在XHR对象实例被实例化之后,它向服务器发送请求的过程中,同时执行其他代码(比如UI渲染)。
在XMLHTTPRequest中,回调方法分为两种:同步回调和异步回调。同步回调指在send()函数立即返回时就执行,而异步回调则是等到收到响应时再执行。使用回调方法的好处是我们可以更好地控制XHR对象的状态,同时避免页面假死。
常见的回调函数有:
onreadystatechange(): 每当readyState属性变化时就会触发该回调函数。onload(): 当请求成功完成时触发该回调函数(即xhr.readyState === XMLHttpRequest.DONE)。ontimeout(): 当请求超时时触发该函数。onerror(): 当请求出现错误时触发该函数。
总之,XMLHttpRequest对象是执行Ajax编程的重要组成部分,而回调方法则是保证XHR对象不阻塞JavaScript线程以及实现异步思维的关键。
181、Js中的fetch方法
fetch()方法是JavaScript中用于发送HTTP请求的现代API,支持Promise,较XMLHttpRequest有更好的设计、更简洁的语法和更强大的功能。
fetch()可以在浏览器环境中通过window.fetch()使用,也可以在Node.js环境中通过引入node-fetch库使用。
fetch(url, options)
.then(response => {
// 对响应进行处理
})
.catch(error => {
// 捕获请求或响应过程中出现的错误
});
其中url参数是必须的,表示请求的目标地址;而options参数是可选的,表示请求的一些配置项,具体包括:
method: 请求方法,比如GET、POST等,默认是GET;headers: 请求头对象,可以设置Content-Type、Authorization等;body: 请求体,用于POST请求;mode: 请求模式,可以设置为basic、cors、opaque等;cache: 缓存模式,可以设置为default、no-store、reload等;redirect: 重定向模式,可以是follow、error、manual等。
fetch()返回一个Promise对象,该对象代表了服务器响应的结果。在成功获取到响应的情况下,会将一个Response对象传递给then回调函数。在响应状态码不是200时,会把一个错误对象传递给catch回调函数。
需要注意的是,如果我们需要以JSON形式获取响应,可以使用response.json()方法,它也返回一个Promise对象。同样的,如果需要获取字符串形式的响应,则可以使用response.text()方法。
总之,fetch()是一个现代的、强大的API,它支持跨域请求、发送文件和FormData等,可以让我们更方便地处理请求和响应,开发高效和优质的网络应用程序。
182、浏览器的Eventloop 和 node的Eventloop区别
浏览器和 Node.js 中的 Event Loop 都是 JavaScript 运行环境的一部分,但它们之间存在一些区别。
- 实现方式不同
在浏览器中,Event Loop 是由浏览器内核提供的。在不同的浏览器中,实现方式可能会有所不同。
在 Node.js 中,Event Loop 是由 libuv 库提供的。
- 宏任务和微任务不同
在浏览器中,事件循环中分为宏任务(macro-task)和微任务(micro-task)。
- 宏任务包括 script(整体代码)、setTimeout、setInterval、I/O、UI 交互事件等等。
- 微任务包括 Promise.then()、MutaionObserver、process.nextTick(Node.js 中独有)等等。
在 Node.js 中,事件循环中也存在宏任务和微任务。但是微任务它仅有 process.nextTick 和 Promise ,即 Node.js 中没有像浏览器中的 MutationObserver 类似的 API 可以用于创建微任务。
- 触发时机不同
在浏览器中,每一个时间循环 Tick 的执行顺序通常为:
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到了微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前 Tick 执行完毕,开始检查渲染,然后 GUI 线程接管渲染
- 渲染完毕之后,JS 线程继续接管,开始下一个 Tick,重复上述步骤
在 Node.js 中,事件循环的执行顺序如下:
- timers 阶段:执行 setTimeout() 和 setInterval() 设定的时间回调函数。
- pending callbacks 阶段:执行系统级别操作的回调函数(例如完成 TCP 连接后的回调函数)
- idle, prepare 阶段:仅 node 内部使用。
- poll 阶段:获取新的 I/O 事件,执行与 I/O 相关的回调函数 (除了 close 事件,它有自己的处理方式,不在这个阶段处理)。
- check 阶段:执行 setImmediate() 回调函数。
- close callbacks 阶段:执行 socket、文件等关闭的回调函数。
可以看出,在浏览器中,每个事件循环 Tick 都会执行完所有微任务再次执行宏任务;而在 Node.js 中,则是在每个阶段执行完毕后才开始执行微任务,即微任务会在每个阶段之间执行。同时,Node.js 还有一个与浏览器不同的 setImmediate() API,它会在当前阶段结束后立即执行回调函数。
总结:虽然浏览器和 Node.js 中的 Event Loop 都是 JavaScript 运行环境的一部分,但它们之间存在一些区别。比如宏任务和微任务的定义不同、触发时机不同等。因此,在编写跨平台 JS 代码时,需要注意其兼容性问题。
183、script都要等待吗?
在HTML文档中,<script>标签用于引入JavaScript脚本。默认情况下,浏览器在解析HTML时遇到<script>标签会执行以下操作:
-
阻塞HTML解析:浏览器会暂停解析HTML文档,直到脚本下载并执行完成。这是因为早期的JavaScript脚本可能会修改DOM。
-
下载脚本:浏览器发起网络请求下载脚本文件。
-
执行脚本:下载完成后,浏览器执行脚本。如果脚本是外部的(即
src属性指定了一个URL),则执行前会先解析并构建该脚本的DOM。 -
继续HTML解析:脚本执行完成后,浏览器继续解析HTML文档。
然而,这种阻塞行为可以通过<script>标签的属性来改变:
-
async属性:当<script>标签包含async属性时,脚本会异步下载,不会阻塞HTML解析。下载完成后,脚本会立即执行,但仍然可能阻塞后续的脚本执行。 -
defer属性:当<script>标签包含defer属性时,脚本会异步下载,不会阻塞HTML解析。与async不同,defer脚本会等到文档解析完成后,按照它们在文档中出现的顺序执行。
以下是<script>标签不同属性对加载和执行行为的影响的表格:
<!-- 阻塞加载,执行时机未知,可能在解析DOM时阻塞 -->
<script src="script1.js"></script>
<!-- 异步加载,不阻塞HTML解析,执行时机在下载完成后,可能在文档解析之前或之后 -->
<script async src="script2.js"></script>
<!-- 异步加载,不阻塞HTML解析,执行时机在文档解析完成后,按照在文档中出现的顺序 -->
<script defer src="script3.js"></script>
最佳实践:
- 如果脚本不依赖于DOM,可以使用
async属性来避免阻塞。 - 如果脚本之间有依赖关系,或者需要在DOM完全解析后执行,使用
defer属性。 - 对于内联脚本(即直接在
<script>标签中编写JavaScript代码),通常不需要async或defer属性,除非脚本非常短,且不依赖于页面其他部分的执行。
184、什么是Js中的原型及原型链
JavaScript中的每个对象都有一个原型(prototype),代表了该对象所继承的属性和方法。当我们在访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,那么JavaScript引擎会自动到该对象的原型中查找,如果找到了相应的属性或方法,则会返回对应的值或执行对应的方法。
每个JavaScript对象都有一个**proto属性,指向它的原型。如果一个对象的原型又有原型,那么它的proto**属性就指向了其原型的原型,依次类推,形成了一个原型链。
最终,所有的对象都可以追溯到Object.prototype这个顶层原型。
使用构造函数来创建JavaScript中的对象
我们可以使用构造函数来创建JavaScript中的对象,并通过给构造函数的prototype属性赋值来创建该构造函数实例的原型,例如:
// 构造函数Animal
function Animal(name) {
this.name = name;
}
// 为Animal的原型添加方法speak
Animal.prototype.speak = function() {
console.log(this.name + ' makes a noise.');
};
// 创建实例cat和dog
let cat = new Animal('Miaow');
let dog = new Animal('Wangwang');
// 调用cat和dog的speak方法
cat.speak(); // Miaow makes a noise.
dog.speak(); // Wangwang makes a noise.
在以上代码中,Animal是一个构造函数,cat和dog是通过该构造函数创建的两个实例。通过给Animal的原型添加方法speak,我们可以让cat和dog都拥有该方法。
185、Js中原型链的终点是什么
在JavaScript中,所有对象都有一个原型(prototype),它们可以继承原型对象的属性和方法。对象的原型对象也可以有自己的原型对象,这就形成了原型链(prototype chain)。
原型链的终点是Object.prototype对象,任何一个对象的原型链最终都会指向它。Object.prototype是所有原型链的顶端,它是JavaScript内置对象的基础,所有内置对象都继承自它。Object.prototype对象提供了一些共享的属性和方法,例如toString()、valueOf()等,这些属性和方法对所有对象都是可用的。如果在自己的对象中找不到某个属性或方法,JavaScript引擎就会沿着其原型链往上查找,直到找到Object.prototype对象为止。
由于Object.prototype是所有对象的祖先,因此它的原型链为空。
需要注意的是,从Object.prototype继承来的属性和方法,可能会被某些内置对象所覆盖,比如在Array对象中重写toString()方法返回不同的结果。因此,在使用原型链时,需要注意对继承来的属性和方法进行覆盖或扩展的可能性。
186、Js调用new的过程
在JavaScript中,当我们使用new操作符来创建一个对象时,实际上是执行了以下几个步骤:
- 创建一个新对象。
- 将该对象的原型指向构造函数的prototype属性。
- 执行构造函数,并将this指向该新对象。
- 如果构造函数返回一个对象,则返回该对象;否则返回该新对象。
下面是一个简单的例子,演示了如何使用new操作符创建一个新的对象:
// 定义一个构造函数Person
function Person(name, age) {
this.name = name;
this.age = age;
}
// 使用new操作符创建一个新对象p1
let p1 = new Person('Tom', 20);
console.log(p1); // {name: "Tom", age: 20}
在以上代码中,我们定义了一个构造函数Person,该函数有两个参数name和age。当我们使用new操作符创建一个新的对象p1时,JavaScript引擎会按照上述步骤来执行。
首先,创建一个新对象{},并将该对象的原型指向Person.prototype。接着,执行构造函数Person,并将this指向该新对象。在构造函数中,我们给新对象p1添加了两个属性name和age。最后,因为构造函数没有返回任何值,所以返回该新对象p1。
通过调用new操作符,我们可以方便地创建一个自定义的对象,并在对象创建时对其进行初始化。在实际应用中,我们可以根据需要编写多个不同的构造函数,用于创建不同类型的对象,并在对象创建时完成一些初始化操作。
187、说说面向对象的特性与特点
面向对象编程(Object-oriented programming,简称 OOP)的三个主要特性是封装性、继承性和多态性,它们分别具有以下特点:
- 封装性
封装性是指将对象的属性和方法都包装在对象内部,对外只暴露必要的接口,外界无法直接访问对象的内部实现细节。这样可以避免对象的属性或方法被意外修改、误用或者泄露,同时可以提高代码的可维护性和安全性。
封装性的特点包括:
- 隐藏对象的内部实现细节,只对外提供必要的接口。
- 可以通过访问器来控制属性的读写操作。
- 类的封装性也可以通过模块化实现,将公共功能封装在一个模块中,不对外暴露私有变量和方法。
- 继承性
继承性是指子类可以从父类继承属性和方法。子类可以通过继承来复用父类的代码,并且可以在继承的基础上进行扩展和改进,减少重复性的代码。同时,继承也是多态性的基础。
继承性的特点包括:
- 子类可以继承父类的属性和方法,包括公共的和受保护的成员。
- 子类可以重写父类的方法来实现自己的功能。
- 子类可以通过 super 关键字调用父类的构造函数和方法。
- 多态性
多态性是指同一操作作用于不同的对象上时,可以产生不同的行为和结果。在 OOP 中,多态性通常表现为父类引用可以指向子类对象,通过动态绑定实现运行时的多态性。
多态性的特点包括:
- 父类引用可以指向子类对象,不同的子类对象会表现出不同的行为。
- 可以通过方法重载和方法重写来实现编译时和运行时的多态性。
- 多态性也可以通过接口实现,一个对象可以实现多个接口,并且可以根据需要转换为不同的接口类型。
以上三个特性是面向对象编程中最基本的特性和核心概念,它们相辅相成、互相依存。封装性可以保证程序的安全性和可维护性,继承性可以提高代码的复用性和扩展性,而多态性可以更好地应对复杂的业务需求和变化。
188、Js中实现继承几种方式
- 1、原型链继承:将父类的实例作为子类的原型,通过 prototype 进行继承
- 2、构造继承:将父类的实例属性复制给子类,通过 call 进行继承
- 3、实例继承:为父类实例添加新特性,作为子类实例返回
- 4、拷贝继承:将父类实例通过循环拷贝给子类
- 5、组合继承:就是 原型链继承 和 构造继承,一起使用
- 6、寄生组合继承:通过寄生方式,砍掉父类的实例属性,避免了 组合继承中,在调用两次父类的构造时,初始化两次实例方法/属性 的缺点
技术详解
在JavaScript中,实现继承的方式有以下几种:
原型链继承:通过将子类的原型指向父类的实例来实现继承。这种方式存在的问题是,所有子类实例共享同一个父类实例,容易造成属性共享和修改父类属性的问题。
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child() {
this.type = 'child';
}
Child.prototype = new Parent();
var child = new Child();
child.sayName(); // 'parent'
借用构造函数继承:通过在子类构造函数中调用父类构造函数来实现继承。这种方式可以避免属性共享的问题,但是无法继承父类原型上的方法。
function Parent() {
this.name = 'parent';
}
function Child() {
Parent.call(this);
this.type = 'child';
}
var child = new Child();
console.log(child.name); // 'parent'
组合继承:将原型链继承和借用构造函数继承结合起来,既可以继承父类原型上的方法,又可以避免属性共享的问题。
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child() {
Parent.call(this);
this.type = 'child';
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
var child = new Child();
child.sayName(); // 'parent'
console.log(child.name); // 'parent'
原型式继承:通过创建一个临时构造函数并将其原型设置为父类实例来实现继承。这种方式类似于原型链继承,但是可以避免创建不必要的父类实例。
function createObject(obj) {
function F() {}
F.prototype = obj;
return new F();
}
var parent = {
name: 'parent',
sayName: function() {
console.log(this.name);
}
};
var child = createObject(parent);
child.name = 'child';
child.sayName(); // 'child'
寄生式继承:在原型式继承的基础上,增强新对象,返回构造函数的方式来实现继承。
function createObject(obj) {
function F() {}
F.prototype = obj;
return new F();
}
function createChild(parent) {
var child = createObject(parent);
child.name = 'child';
child.sayName = function() {
console.log(this.name);
};
return child;
}
var parent = {
name: 'parent',
sayName: function() {
console.log(this.name);
}
};
var child = createChild(parent);
child.sayName(); // 'child'
寄生组合式继承:在组合继承的基础上,使用Object.create()方法来创建父类原型的副本,避免了调用父类构造函数时创建不必要的实例。
function inherit(child, parent) {
var prototype = Object.create(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
function Parent() {
this.name = 'parent';
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child() {
Parent.call(this);
this.type = 'child';
}
inherit(Child, Parent);
var child = new Child();
child.sayName(); // 'parent'
console.log(child.name); // 'parent'
189、Js中数据类型有哪些
JS 中的数据类型分为 值类型(基本类型) 和 引用类型(对象类型)
- 基本类型有:
undefined、null、boolean、string、number、symbol、BigInt七种 - 引用类型有:
Object、Function、Array、RegExp、Date等等
其中 Symbol 和 BigInt 是ES6 中新增的数据类型:
- Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
- BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。
190、Js中引用数据类型和基本数据类型的区别是什么
-
存储方式: 基本数据类型的值直接存储在变量中,而引用数据类型的值存储在堆内存中,并通过引用地址保存到变量中。
-
复制方式: 基本数据类型的赋值是复制变量的值,而引用数据类型的赋值是复制变量的引用地址。这意味着当一个引用类型的变量被赋值给另一个变量时,两个变量将引用同一个对象。
-
比较方式: 基本数据类型的比较是比较它们的值是否相等,而引用数据类型的比较是比较它们的引用地址是否相等。即使两个对象具有相同的属性和值,但它们的引用地址不同,也会被认为是不相等的。
-
可变性: 基本数据类型是不可变的,一旦创建就不能修改其值。而引用数据类型是可变的,可以修改对象的属性或方法。
需要注意的是,虽然字符串、数字和布尔值在JavaScript中属于基本数据类型,但它们有对应的包装对象(String、Number和Boolean),可以调用一些额外的方法。这种自动转换称为"装箱"和"拆箱"。
191、Js中null 和 undefined 有什么区别
在 JavaScript 中,null 和 undefined 都可以用来表示“无”这个概念,但是它们在语义表达和实际使用上存在区别。具体如下:
- 定义:undefined 表示“未定义”,即声明了一个变量但未赋值时,该变量的默认值就是 undefined;而 null 表示“空值”,即表示一个没有值的对象指针。
- 转换:undefined 是一个预定义的全局变量,其值为 undefined。当一个变量被声明但未被赋值时,其默认值为 undefined。如果把 undefined 变量进行类型转换,它会被转换成 false。而 null 在进行类型转换时,会被转换成数字 0。
- 比较:在进行比较时,null 会被认为等于 undefined,但是它们的类型不同。例如,null == undefined 的结果为 true,但是 null === undefined 的结果为 false。
- 使用:undefined 表示变量还未被定义,或者某个属性不存在。在实际开发中,通常用 void 0 来获取 undefined 值。而 null 通常表示从业务上讲某个变量为空值,例如在操作 DOM 对象时,如果无法找到目标元素,其返回值就是 null。
192、什么是Js中Object属性的可枚举性
在 JavaScript 中,每个对象都有一组属性,每个属性都有一个可枚举性(enumerable)属性描述符。
这个描述符指示了该属性是否可以被枚举出来,通常可以通过 for...in 循环或 Object.keys() 方法来枚举对象属性。
可枚举属性有两个主要的用途:
-
遍历属性:通过
for...in循环或者Object.keys()方法来遍历对象的所有可枚举属性。 -
序列化对象:将对象转化为 JSON 字符串时,只有那些可枚举属性被包括在序列化结果中。
在 JavaScript 中,对象的属性默认是可枚举的,即 enumerable 属性默认为 true。但也可以通过 Object.defineProperty() 或 Object.defineProperties() 方法来修改属性的描述符,从而改变属性的特性,包括是否可枚举。
举个例子:
const person = {
name: 'Tom',
age: 20,
gender: 'male'
};
console.log(Object.keys(person)); // ['name', 'age', 'gender']
Object.defineProperty(person, 'gender', {
enumerable: false,
});
console.log(Object.keys(person)); // ['name', 'age']
在这个例子中,我们先创建了一个对象 person,它包含三个属性:name、age 和 gender,其中 gender 属性的 enumerable 属性默认为 true。
然后我们使用 Object.defineProperty() 方法来修改 gender 属性的 enumerable 属性为 false,这意味着 gender 属性不会被 for...in 循环或者 Object.keys() 方法枚举出来。
最后我们调用 Object.keys() 方法来获取 person 对象所有可枚举的属性名,发现 gender 属性已经不在对象的可枚举属性列表中了。
总而言之,对象属性的可枚举性用于控制对象属性是否可以被遍历和序列化。通过定义属性的描述符,我们可以很方便地修改属性的可枚举性。
193、什么是 Js 中的包装类型
JavaScript 中的包装类型,是指在对基本类型进行属性或方法操作时,JavaScript 引擎会自动将其转换成对应的对象类型,从而实现基本类型值的一些额外操作。这些对象类型被称为“包装类型”。
JavaScript 中的基本类型包括 undefined、null、boolean、number 和 string,对于这些基本类型值进行属性或方法操作时,JavaScript 引擎会自动创建对应的包装类型对象,例如:
let str = "hello";
console.log(str.length); // 输出 5
// 等价于以下代码
let tempStr = new String(str);
console.log(tempStr.length);
其中,String() 是字符串类型的构造函数,通过这个构造函数创建的对象,就是字符串类型的包装类型对象。同样,JavaScript 中还有 Boolean()、Number() 等构造函数,对应着布尔类型和数值类型的包装类型对象。
需要注意的是,对于基本类型值和对应的包装类型对象来说,并不是完全相等的。例如:
let str1 = "hello";
let str2 = new String("hello");
console.log(typeof str1); // 输出 "string"
console.log(typeof str2); // 输出 "object"
console.log(str1 === "hello"); // 输出 "true"
console.log(str2 === "hello"); // 输出 "false"
console.log(str2.toString() === "hello"); // 输出 "true"
194、Js中为什么0.1+0.2 ! == 0.3,如何让其相等 (精度丢失)
这是因为在 JavaScript 中,浮点数的精度问题。0.1 和 0.2 转化为二进制小数后都是无限循环小数,而 JavaScript 内部使用 64 位双精度浮点数格式(IEEE 754 标准),只能支持一定位数(一般为 53 位)的精度,所以在进行浮点数计算时可能会出现精度误差。
具体来说,0.1 的二进制小数为 0.0001100110011...(无限循环),0.2 的二进制小数为 0.001100110011...(无限循环),而 0.1 + 0.2 的结果的二进制小数为 0.010011001100...(也是无限循环,但是与简单的 0.3 不同),在转换为十进制时,由于精度的限制,结果被截断成了最接近的值,即 0.30000000000000004,与 0.3 不相等。
195、Js中{}和 [] 的 valueOf 和 toString 的结果是什么
{} 的 valueOf 结果为 {} ,toString 的结果为 "[object Object]"
[] 的 valueOf 结果为 [] ,toString 的结果为 ""
196、Js中for in 和 for of 的区别
for in 循环用于遍历对象中的所有可枚举属性,包括实例属性和原型属性。可以使用 for in 循环来遍历对象的属性名称并访问对应的属性值。
需要注意的是,for in 循环会遍历对象的原型链上所有可枚举的属性,因此我们需要使用 Object.prototype.hasOwnProperty() 方法来判断该属性是否为对象自身的属性。
for (let prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
console.log(`${prop}: ${obj[prop]}`);
}
}
for of 循环用于遍历数组、类数组对象、字符串、Set、Map 等可迭代对象中的元素。for of 循环会遍历对象的元素值而非属性名称。
需要注意的是,只有实现了迭代器(Iterator)接口的对象才能被 for of 循环遍历。迭代器接口是由一个 next() 方法组成的对象,每次调用 next() 方法都会返回一个具有 value 和 done 属性的对象。其中,value 属性为当前元素的值,done 属性表示是否已经遍历完成。
总结
for in 和 for of 是 JavaScript 中常用的迭代语句,用于遍历对象或数组中的元素。for in 循环用于遍历对象的属性名称,而 for of 循环用于遍历数组、类数组对象、字符串、Set、Map 等可迭代对象中的元素值。需要注意的是,在使用 for in 循环时需要通过 Object.prototype.hasOwnProperty() 方法判断属性是否为对象自身的属性,而在使用 for of 循环时需要使用实现了迭代器接口的对象。
197、Js中includes和indexOf的区别
includes() 和 indexOf() 都是用来检查数组是否包含某些元素。它们的区别在于:
includes()方法返回一个布尔值,表示数组是否包含给定的值,而indexOf()方法返回第一个匹配项的下标,如未找到则返回 -1。- 在判断数组中是否包含
NaN时,includes()方法会返回true,而indexOf()方法会返回-1,这是因为在 JavaScript 中,NaN和任何值都是不相等的,所以不能使用普通的比较运算符来比较NaN的值。
总的来说,当我们需要判断一个数组中是否包含某个元素时,使用 includes() 方法会更方便和直观一些,尤其是对于简单数据类型(如字符串、数字等)的数组。但如果需要获取元素的下标或者需要通过回调函数进行复杂判断时,则需要使用 indexOf() 方法。
198、Js中bind(),call()和apply()的区别
JavaScript 中的 bind()、call() 和 apply() 方法都可以用来改变函数内部的 this 指向。
它们有一些重要的区别:
- 三种方法最大的区别在于参数传入方式不同:bind() 方法接受一系列参数列表,而 call() 和 apply() 方法则分别接受一组参数和一个参数列表。具体而言,bind() 将参数作为一个个单独的值传入,而 call() 和 apply() 都允许传递一个数组作为参数。
- 执行时间不同:bind() 绑定后返回一个新函数,并不会立即执行,需要调用该函数才会执行;而 call() 和 apply() 则会立即执行函数。
- 返回值不同:bind() 方法返回一个绑定后的新函数,而 call() 和 apply() 则直接执行原始函数并返回执行结果。
它们的作用分别如下:
- bind() 方法:bind() 可以指定函数内部的 this 指向,并将其绑定到一个新函数上进行返回。该函数并不会立即执行,而是等待调用。bind() 也可以用来实现柯里化(currying)
- call() 方法:call() 可以在指定的 this 值和若干个参数(参数的列表)的前提下调用某个函数或方法。注意,call() 方法需要将参数逐个传递进去,而不能像 apply() 方法一样将所有参数打包成一个数组。
- apply() 方法:apply() 和 call() 的作用非常类似,都是改变函数内部的 this 指向。区别在于,apply() 方法需要将参数打包成一个数组传递进去,而 call() 则是将参数逐个传递。
199、如何判断Js中 this 的指向
- 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
- 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
- 第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
- 第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。