JavaScript 常见大坑与细节

执行环境(Execution context)

var 和 let 的正确解释

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

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

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

var a = ‘Hello world‘

function b() {
    console.log(‘call b‘)
}

想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是创建的阶段,JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。

在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升

b() // call b second

function b() {
    console.log(‘call b fist‘)
}
function b() {
    console.log(‘call b second‘)
}
var b = ‘Hello world‘

var 会产生很多错误,所以在 ES6中引入了 let。let 不能在声明前使用,但是这并不是常说的 let 不会提升,let 提升了,在第一阶段内存也已经为他开辟好了空间,但是因为这个声明的特性导致了并不能在声明前使用。

作用域

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 毫秒,不足会自动增加。

以下输出建立在 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

很多时候跟着书和网站查找资料学习,会发现没有目标,学了很多却不知道自己到底能够做出什么成绩。要有一个清晰的职业学习规划,学习过程中会遇到很多问题,你可以到我们的前端学习交流Q—q-u-n【 731771211 】,基础,进阶。从企业招聘人才需求 到怎么学习前端开发,和学习什么内容都有免费系统分享,让你无论是自学还是找相应的培训都能让你少走弯路。希望可以帮助你快速了解前端,学习前端

自己从事前端开发五年了,希望能帮助大家更好的学习前端

点击:加入

原文地址:http://blog.51cto.com/14082686/2346897

时间: 2024-11-13 08:58:35

JavaScript 常见大坑与细节的相关文章

JavaScript常见技术点

今天看到一篇博客讲解了几个JavaScript的技术点,感觉很实用. 原地址:Javascript常见技术点 1.javascript面向对象中继承实现 javascript面向对象中的继承实现一般都使用到了构造函数和Prototype原型链,简单的代码如下: <span style="font-family:Microsoft YaHei; font-size:12px"> function Animal(name) { this.name = name; } Anima

javascript常见的设计模式举例

    近日重读<javascript面型对象编程指南>这本书,最后一章介绍了常见的javascript设计模式的实现.主要讲解了四种设计模式:单例模式.工厂模式.装饰器模式和观察者模式.js作为动态语言,实现这四种模式的实例相对简单,当然既然称之为模式,那么吃透思想更重要,那么下面,由乐帝来实例讲解四种模式.    1.单例模式    顾名思义,对象构造出来的是实例,从字面上理解,单例即单实例,这意味一个类只能创建一个实例对象.当需要创建一种类型或者一个类的唯一对象时,可使用该模式.以下两个

js验证网址等Javascript常见验证代码合集

发一个利用js验证网址是否正确,email格式是否正确,是否为数字及数字的范围,密码或字符长度及是否相等及要求的最小字符串长度,输入是否为空等Javascript常见验证代码合集,用的上的朋友可以拿去了自行添加整理. 关键的JavaScript代码函数: 查看代码 打印 001 /** 002 * 数据验证框架.增加了对id字段检查出错时,直接在对应后面添加一< span>元素来显示错误信息. 003 * 004 * @author www.phpernote.com 005 * @versi

JavaScript中的一些细节

1.设置id / class等属性 用 setAttribute 设置一些常规属性如 id ,className 的时候经常不起作用,只能用 object.id = value 这样来设置 news_item.id="news"+i; 2.DOM的一个实用功能是几乎所有Document对象实现的搜索方法同时也能被HTMLElement对象实现,唯一的例外是getElementById.只有document对象才能使用getElementById.若其它对象使用此方法则出错. var n

Javascript变量名混淆细节

前言 UglifyJS会对JS文件的变量名进行混淆处理,要理解Javascript变量混淆的细节,我们需要回答以下几个问题: 1.遇到一个变量myName,我们怎么知道这个myName变量要不要混淆 2.混淆名字怎么生成才合适,新的名字替换旧的名字时有什么要注意的地方? 3.哪些关键字会产生一个作用域? 4.作用域链跟符号表在UglifyJS里边是怎么体现? 5.UglifyJS混淆的过程是什么样? 我们先梳理一下这5个问题,最后贴出我阅读UglifyJS在这部分的实现时做的代码注释. 1.遇到

【javascript】javascript常见正则表达式实例

javascript常见正则表达式实例 实例来源 1 var myRegExp = { 2 // 检查字符串是否为合法QQ号码 3 isQQ: function(str) { 4 // 1 首位不能是0 ^[1-9] 5 // 2 必须是 [5, 11] 位的数字 \d{4, 9} 6 var reg = /^[1-9][0-9]{4,9}$/gim; 7 if (reg.test(str)) { 8 console.log('QQ号码格式输入正确'); 9 return true; 10 }

javascript常见编程模式举例

最近买到手了一本<javascript框架设计>,详细介绍开发js框架所用到的知识.初读一点,乐帝脆弱的理论修养就暴露无遗了,所以专门加强理论修养,重看javascript编程模式的举例.下面来介绍下js中,常见的编程模式.    1.命名空间    同其他高级语言一样,js中的命名空间概念,也是为了减少命名冲突,但js没有命名空间关键字.js实现命名空间的思路是定义一个全局变量,将此命名空间的变量和方法,定义为这个全局变量的属性. var MYAPP=MYAPP||{};//全局变量 MYA

JavaScript 常见安全漏洞及自动化检测技术

序言 随着 Web2.0 的发展以及 Ajax 框架的普及,富客户端 Web 应用(Rich Internet Applications,RIA)日益增多,越来越多的逻辑已经开始从服务器端转移至客户端,这些逻辑通常都是使用 JavaScript 语言所编写.但遗憾的是,目前开发人员普遍不太关注 JavaScript 代码的安全性.据 IBM X-Force 2011 年中期趋势报告揭示,世界五百强的网站及常见知名网站中有 40% 存在 JavaScript 安全漏洞.本文将结合代码向读者展示常见

JavaScript —— 常见用途

javaScript 简介 第一个JavaScript 程序: 点击按钮显示日期   <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>菜鸟教程(runoob.com)</title> <script> function displayDate(){ document.getElementById("demo").