带你理解 JS 容易出错的坑和细节

目前本身组建的一个团队正在写一份面试图谱,将会在七月中旬开源。内容十分丰富,初版会开源前端方面知识和程序员必备知识,后期会逐步写入后端方面知识。由于工程所涉及内容太多(目前已经写了一个半月),而且还需翻译成英文,因此所需时间较长。有兴趣的同窗能够 Follow 个人 Github 获得最快的更新消息。javascript

执行环境(Execution context)

var 和 let 的正确解释

当执行 JS 代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。前端

接下来让咱们看一个老生常谈的例子,varjava

b() // call b
console.log(a) // undefined

var a = 'Hello world'

function b() {
    console.log('call b')
}
复制代码

想必以上的输出你们确定都已经明白了,这是由于函数和变量提高的缘由。一般提高的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于你们理解。可是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是建立的阶段,JS 解释器会找出须要提高的变量和函数,而且给他们提早在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明而且赋值为 undefined,因此在第二个阶段,也就是代码执行阶段,咱们能够直接提早使用。git

在提高的过程当中,相同的函数会覆盖上一个函数,而且函数优先于变量提高程序员

b() // call b second

function b() {
    console.log('call b fist')
}
function b() {
    console.log('call b second')
}
var b = 'Hello world'
复制代码

var 会产生不少错误,因此在 ES6中引入了 letlet 不能在声明前使用,可是这并非常说的 let 不会提高,let 提高了,在第一阶段内存也已经为他开辟好了空间,可是由于这个声明的特性致使了并不能在声明前使用。github

做用域

function b() {
    console.log(value)
}

function a() {
    var value = 2
    b()
}

var value = 1
a()
复制代码

能够考虑下 b 函数中输出什么。你是否会认为 b 函数是在 a 函数中调用的,相应的 b 函数中没有声明 value 那么应该去 a 函数中寻找。其实答案应该是 1。面试

当在产生执行环境的第一阶段时,会生成 [[Scope]] 属性,这个属性是一个指针,对应的有一个做用域链表,JS 会经过这个链表来寻找变量直到全局环境。这个指针指向的上一个节点就是该函数声明的位置,由于 b 是在全局环境中声明的,因此 value 的声明会在全局环境下寻找。若是 b 是在 a 中声明的,那么 log 出来的值就是 2 了。后端

异步

JS 是门同步的语言,你是否疑惑过那么为何 JS 还有异步的写法。其实 JS 的异步和其余语言的异步是不相同的,本质上仍是同步。由于浏览器会有多个 Queue 存放异步通知,而且每一个 Queue 的优先级也不一样,JS 在执行代码时会产生一个执行栈,同步的代码在执行栈中,异步的在 Queue 中。有一个 Event Loop 会循环检查执行栈是否为空,为空时会在 Queue 中查看是否有须要处理的通知,有的话拿到执行栈中去执行。数组

function sleep() {
  var ms = 2000 + new Date().getTime()
  while( new Date() < ms) {}
  console.log('sleep finish')
}

document.addEventListener('click', function() {
  console.log('click')
})

sleep()
setTimeout(function() {
    console.log('timeout');
}, 0);

Promise.resolve().then(function() {
    console.log('promise');
});
console.log('finish')
复制代码

以上代码若是你在 sleep 被调用期间点击,只有当 sleep 执行结束而且 log finish 后才会响应其余异步事件。因此要注意 setTimeout 并非你设定多久 JS 就会准时的响应,而且 setTimeout 也有个小细节,第二个参数设置为 0 也许会有人认为这样就不是异步了,其实仍是异步。这是由于 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增长。promise

如下输出创建在 Chrome 上,不一样的浏览器会有不一样的输出

promise // promise 会进入 Microtask Queue 中,这个 Queue 会优先执行
timeout // setTimeout 会进入 task Queue 中
click // 点击事件会进入 Event Queue 中
复制代码

类型

原始值

JS 共有 6 个原始值,分别为 Boolean, Null, Undefined, Number, String, Symbol,这些类型都是值不可变的。

有一个易错的点是:虽然 typeof null 是 object 类型,可是 Null 不是对象,这是 JS 语言的一个好久远的 Bug 了。

深浅拷贝

对于对象来讲,直接将一个对象赋值给另一个对象就是浅拷贝,两个对象指向同一个地址,其中任何一个对象改变,另外一个对象也会被改变

var a = [1, 2]
var b = a
b.push(3)
console.log(a, b) // -> 都是 [1, 2, 3]
复制代码

有些状况下咱们可能不但愿有这种问题,那么深拷贝能够解决这个问题。深拷贝不只将原对象的各个属性逐个复制出去,并且将原对象各个属性所包含的对象也依次采用深复制的方法递归复制到新对象上。深拷贝有多种写法,有兴趣的能够看这篇文章

函数和对象

this

this 是不少人会混淆的概念,可是其实他一点都不难,你只须要记住几个规则就能够了。

function foo() {
  console.log(this.a)
}
var a = 2
foo() 

var obj = {
  a: 2,
  foo: foo
}
obj.foo() 

// 以上二者状况 this 只依赖于调用函数前的对象,优先级是第二个状况大于第一个状况

// 如下状况是优先级最高的,this 只会绑定在 c 上
var c = new foo()
c.a = 3
console.log(c.a)

// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new
复制代码

以上几种状况明白了,不少代码中的 this 应该就没什么问题了,下面让咱们看看箭头函数中的 this

function a() {
    return () => {
        return () => {
            console.log(this)
        }
    }
}
console.log(a()()())
复制代码

箭头函数实际上是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this。在这个例子中,由于调用 a 符合前面代码中的第一个状况,因此 this 是 window。而且 this 一旦绑定了上下文,就不会被任何代码改变。

下面咱们再来看一个例子,不少人认为他是一个 JS 的问题

var a = {
    name: 'js',
    log: function() {
        console.log(this)
        function setName() {
            this.name = 'javaScript'
            console.log(this)
        }
        setName()
    }
}
a.log()
复制代码

setName 中的 this 指向了 window,不少人认为他应该是指向 a 的。这里其实咱们不须要去管函数是写在什么地方的,咱们只须要考虑函数是怎么调用的,这里符合上述第一个状况,因此应该是指向 window。

闭包和当即执行函数

闭包被不少人认为是一个很难理解的概念。其实闭包很简单,就是一个可以访问父函数局部变量的函数,父函数在执行完后,内部的变量还存在内存上让闭包使用。

function a(name) {
    // 这就是闭包,由于他使用了父函数的参数
    return function() {
        console.log(name)
    }
}
var b = a('js')
b() // -> js
复制代码

如今来看一个面试题

function a() {
    var array = []

    for(var i = 0; i < 3; i++) {
        array.push(
            function() {
                console.log(i)
            }
        )
    }

    return array
}

var b = a()
b[0]()
b[1]()
b[2]()
复制代码

这个题目由于 i 被提高了,因此 i = 3,当 a 函数执行完成后,内存中保留了 a 函数中的变量 i。数组中 push 进去的只是声明,并无执行函数。因此在执行函数时,输出了 3 个 3。

若是咱们想输出 0 ,1,2 的话,有两种简单的办法。第一个是在 for 循环中,使用 let 声明一个变量,保存每次的 i 值,这样在 a 函数执行完成后,内存中就保存了 3 个不一样 let 声明的变量,这样就解决了问题。

还有个办法就是使用当即执行函数,建立函数即执行,这样就能够保存下当前的 i 的值。

function a() {
    var array = []

    for(var i = 0; i < 3; i++) {
        array.push(
            (function(j) {
                return function() {
                    console.log(j)
                }
            }(i))
        )
    }

    return array
}
复制代码

当即执行函数其实就是直接调用匿名函数

function() {} ()
复制代码

可是以上写法会报错,由于解释器认为这是一个函数声明,不能直接调用,因此咱们加上了一个括号来让解释器认为这是一个函数表达式,这样就能够直接调用了。

因此咱们其实只须要让解释器认为咱们写了个函数表达式就好了,其实还有不少种当即执行函数写法

true && function() {} ()
new && function() {} ()
复制代码

当即执行函数最大的做用就是模块化,其次就是解决上述闭包的问题了。

原型,原型链和 instanceof 原理

原型可能不少人以为很复杂,本章节也不打算重复复述不少文章都讲过的概念,你只须要看懂我画的图而且本身实验下便可

function P() {
    console.log('object')
}

var p = new P()

复制代码

原型链就是按照 __proto__ 寻找,直到 Object。instanceof 原理也是根据原型链判断的

p instanceof P // true
p instanceof Object // true
复制代码

相关文章
相关标签/搜索