JavaScript 类型
7 种语言类型
1.undefined;
2.Null;
3.Boolean;
4.String;
5.Number;
6.Symbol;
7.Object;
为什么有的编程规范要求用 void 0 代替 undefined
Undefined 类型表示未定义,它的类型只有一个值,就是 undefined。任何变量在赋值前是 Undefined 类型、值为 undefined,一般我们可以用全局变量 undefined(就是名为 undefined 的这个变量)来表达这个值,或者 void 运算来把任意一个表达式变成 undefined 值。
但是呢,因为 JavaScript 的代码 undefined 是一个变量,而并非是一个关键字,这是 JavaScript 语言公认的设计失误之一,所以,我们为了避免无意中被篡改,建议使用 void 0 来获取 undefined 值。
MDN 定义: The void operator evaluates the given expression and then returns undefined.意思是说 void 运算符可以对给定的表达式求值,并且无论后面跟的是什么,都是返回 undefined,所以说不论是void 0 还是void 1都是可以的,更重要的是void不能被重写。
Undefined 跟 Null 有一定的表意差别,Null 表示的是:“定义了但是为空”。所以,在实际编程时,我们一般不会把变量赋值为 undefined,这样可以保证所有值为 undefined 的变量,都是从未赋值的自然状态。
Null 类型也只有一个值,就是 null,它的语义表示空值,与 undefined 不同,null 是 JavaScript 关键字,所以在任何代码中,你都可以放心用 null 关键字来获取 null 值。
null是对象吗
虽然typeof null 返回的是object,但是null不是对象,而是基本数据类型的一种。
typeof null // "object" (因为一些以前的原因而不是'null')
typeof undefined // "undefined"
null === undefined // false
null == undefined // true
null === null // true
null == null // true
!null //true
isNaN(1 + null) // false
isNaN(1 + undefined) // true
浮点数的比较
console.log( 0.1 + 0.2 == 0.3);//false
浮点数运算的精度问题导致等式左右的结果并不是严格相等,而是相差了个微小的值.所以实际上,这里错误的不是结论,而是比较的方法,正确的比较方法
console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);//true
-
Number、String 和 Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象,当直接调用时,它们表示强制类型转换。
-
基本数据类型存储在栈内存,存储的是值。复杂数据类型的值存储在堆内存,地址(指向堆中的值)存储在栈内存。当我们把对象赋值给另外一个变量的时候,复制的是地址,指向同一块内存空间,当其中一个对象改变时,另一个对象也会变化。
-
typeof和 instanceof
JavaScript 对象
JavaScript 对象的两类属性
-
数据属性
- value:就是属性的值。
- writable:决定属性能否被赋值。
- enumerable:决定 for in 能否枚举该属性。
- configurable:决定该属性能否被删除或者改变特征值。
-
访问器(getter/setter)属性
- getter:函数或 undefined,在取属性值时被调用。
- setter:函数或 undefined,在设置属性值时被调用。
- enumerable:决定 for in 能否枚举该属性。
- configurable:决定该属性能否被删除或者改变特征值。
访问器属性使得属性在读和写时执行代码,它允许使用者在写和读属性时,得到完全不同的值,它可以视为一种函数的语法糖。
可以使用内置函数 Object.getOwnPropertyDescripter 来查看:
var o = { a: 1 }; o.b = 2; //a 和 b 皆为数据属性 Object.getOwnPropertyDescriptor(o,"a") // {value: 1, writable: true, enumerable: true, configurable: true} Object.getOwnPropertyDescriptor(o,"b") // {value: 2, writable: true, enumerable: true, configurable: true}
JavaScript原型
谈谈你对原型的理解?
在 JavaScript 中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象。使用原型对象的好处是所有对象实例共享它所包含的属性和方法。
-
什么是原型链?【原型链解决的是什么问题?】
原型链解决的主要是继承问题。
略
-
prototype 和
__proto__区别是什么?prototype是构造函数的属性,
__proto__是每个实例都有的属性,可以访问 [[prototype]] 属性。实例的
__proto__与其构造函数的prototype指向的是同一个对象。function Student(name) { this.name = name; } Student.prototype.setAge = function(){ this.age=20; } let Jack = new Student('jack'); console.log(Jack.__proto__); //console.log(Object.getPrototypeOf(Jack));; console.log(Student.prototype); console.log(Jack.__proto__ === Student.prototype);//true
JavaScript执行
Promise里的代码为什么比setTimeout先执行
Promise 是 JavaScript 语言提供的一种标准化的异步管理方式,它的总体思想是,需要进行 io、等待或者其它异步操作的函数,不返回真实结果,而返回一个“承诺”,函数的调用方可以在合适的时机,选择等待这个承诺兑现(通过 Promise 的 then 方法的回调)。
var r = new Promise(function(resolve, reject){
console.log("a");
resolve()
});
r.then(() => console.log("c"));
console.log("b")
//a
//b
//c
在进入 console.log(“b”) 之前,毫无疑问 r 已经得到了 resolve,但是 Promise 的 resolve 始终是异步操作,即then方法是在当前脚本所有同步任务执行后才执行,所以 c 无法出现在 b 之前。
Promise(function(resolve, reject) {
console.log(2)
for (var i = 0; i < 10000; i++) {
if(i === 10) {console.log(10)}
i == 9999 && resolve();
}
console.log(3)
})
//2 10 3
Promise新建后立即执行,也就是说,Promise构造函数里的代码是同步执行的。
再看看与setTimeout混用
var r = new Promise(function(resolve, reject){
console.log("a");
resolve()
});
setTimeout(()=>console.log("d"), 0)
r.then(() => console.log("c"));
console.log("b")
我们发现,不论代码顺序如何,d 必定发生在 c 之后,因为 Promise 产生的是 JavaScript 引擎内部的微任务,而 setTimeout 是浏览器 API,它产生宏任务。微任务始终先于宏任务。then会比setTimeout先执行。
如何分析异步执行的顺序:
-
首先分析有多少个宏任务;
-
在每个宏任务中,有多少个微任务;
-
根据调用次序,确定宏任务中的微任务的执行次序;
-
根据宏任务的触发规则和调用次序,确定宏任务的执行次序
-
确定整个顺序
function sleep(duration) {
return new Promise(function(resolve, reject) {
console.log("b");
setTimeout(resolve,duration);
})
}
console.log("a");
sleep(5000).then(()=>console.log("c"));
setTimeout把整个代码分成了2个宏观任务,这里不论是 5 秒还是 0 秒,都是一样的。
第一个宏观任务中,包含了先后同步执行的 console.log(“a”); 和 console.log(“b”);。
setTimeout 后,第二个宏观任务执行调用了 resolve,然后 then 中的代码异步得到执行,所以调用了 console.log(“c”),最终输出的顺序才是: a b c。
练习:
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function(resolve, reject) {
console.log(2)
for (var i = 0; i < 10000; i++) {
if(i === 10) {console.log(10)}
i == 9999 && resolve();
}
console.log(3)
}).then(function() {
console.log(4)
})
console.log(5);
答案:2 10 3 5 4 1
相关:async/await
事件循环(event-loop)
JavaScript的运行机制
-
JavaScript的运行机制:
- 所有的同步任务都在主线程上执行,形成一个执行栈;
-
主线程外,还会有个任务队列,只要异步的任务有了运行结果,就在任务队列里放置一个事件;
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行;
- 主线程不断重复上面的第三步
概括即是: 调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作
所以:
一个事件循环中有一个或者是多个任务队列;
JavaScript中有两种异步任务:
-
宏任务: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
-
微任务: process.nextTick(Nodejs), Promises, Object.observe, MutationObserver;
事件循环是什么:
主线程从"任务队列"中读取执行事件,这个过程是循环不断的,这个机制被称为事件循环。此机制具体如下:主线程会不断从任务队列中按顺序取任务执行,每执行完一个任务都会检查微任务队列是否为空(执行完一个任务的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有微任务。然后再进入下一个循环去任务队列中取下一个任务执行。
需要注意的是:当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件, 然后再去宏任务队列中取出一个事件。同一次事件循环中, 微任务永远在宏任务之前执行。
为什么会需要event-loop?
因为 JavaScript 是单线程的。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理(user agent)必须使用事件循环(event loops)。
注意:以上介绍为浏览器的事件循环,node环境中不一定是这样的。
事件循环补充
- setTimeout精度不高的原因
一定要清楚,setTimeout(..)并没有把你的回调函数挂在事件循环队列中。它所做的是设定一个定时器。当定时器到时后,环境会把你的回调函数放在事件循环中,这样,在未来某个时刻的tick会摘下并执行这个回调。 如果这时候事件循环中已经有20个项目了会怎样呢?你的回调就会等待。它得排在其他项目后面——通常没有抢占式的方式支持直接将其排到队首。这也解释了为什么setTimeout(..)定时器的精度可能不高。
- 并行线程:
术语“异步”和“并行”常常被混为一谈,但实际上它们的意义完全不同。记住,异步是关于现在和将来的时间间隙,而并行是关于能够同时发生的事情。 并行计算最常见的工具就是进程和线程。进程和线程独立运行,并可能同时运行:在不同的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存。 与之相对的是,事件循环把自身的工作分成一个个任务并顺序执行,不允许对共享内存的并行访问和修改。通过分立线程中彼此合作的事件循环,并行和顺序执行可以共存。 并行线程的交替执行和异步事件的交替调度,其粒度是完全不同的。