JS归纳(重学前端)

305 阅读9分钟

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先执行。

如何分析异步执行的顺序:

  1. 首先分析有多少个宏任务;

  2. 在每个宏任务中,有多少个微任务;

  3. 根据调用次序,确定宏任务中的微任务的执行次序;

  4. 根据宏任务的触发规则和调用次序,确定宏任务的执行次序

  5. 确定整个顺序

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中有两种异步任务:

    1. 宏任务: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering

    2. 微任务: 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(..)定时器的精度可能不高。

  • 并行线程:

术语“异步”和“并行”常常被混为一谈,但实际上它们的意义完全不同。记住,异步是关于现在和将来的时间间隙,而并行是关于能够同时发生的事情。 并行计算最常见的工具就是进程和线程。进程和线程独立运行,并可能同时运行:在不同的处理器,甚至不同的计算机上,但多个线程能够共享单个进程的内存。 与之相对的是,事件循环把自身的工作分成一个个任务并顺序执行,不允许对共享内存的并行访问和修改。通过分立线程中彼此合作的事件循环,并行和顺序执行可以共存。 并行线程的交替执行和异步事件的交替调度,其粒度是完全不同的。