前言
这是参见 javascript-questions 库做出的一些笔记,记录一下 js 中一些奇奇怪怪的现象和注意事项~
本篇主题有:
- IIFE
- 令人迷惑的 new
- 浏览器事件知识
IIFE
关联至问题 2
题目给出了两个相似的 for 循环,它们唯一的区别就是循环变量的声明方法,一个是使用的是 var
关键字,而另外一个使用的是 ES6 中新增的 let
关键字。在两个循环的循环体内都是使用 setTimeout
方法在延迟一段同一段长度的时间后将循环变量 i
打印到控制台。
奇怪的是,使用 var
声明循环变量的那个循环最终会输出 ‘3’ 三次,而使用 let
声明循环变量会按照我们的期望输出 ‘1’, ‘2’, ‘3’。
这主要是因为两种变量声明方式不同导致的差异:
var
这种声明方式会使变量提升,并且分配内存空间,放在这个问题 2中,在 for 循环执行之后访问循环变量i
,由于这个变量已经成为全局变量了,并且哪怕将这个定时器的延迟设置为 0,setTimeout
的回调也始终会在循环执行完成之后再执行,因此回调执行时,访问到的循环变量i
的值已经是 3 了。let
则会绑定到块级作用域,也就是 for 循环的循环体,每次循环都会将i
的新值绑定到循环体,这么说可能不是很让人明白,但是经过试验可以证明这个绑定仅仅是浅拷贝而已,不过对于基本数据类型还是会发生真正意义上的拷贝,下面补充这个例子证明了这一点:1
2
3
4
5const l = [];
for (let a = { i: 0 }; a.i < 3; a.i++) {
setTimeout(() => l.push(a), 1);
}
console.log(l[0] === l[1] && l[1] === l[2]); // true
这个问题体现出 ES6 中 let
关键字与 var
关键字声明变量的差异,但是在 ES6 之前,JS 程序员该如何实现或模拟块级作用域绑定呢?答案就是 IIFE(立即调用函数表达式):
1 | for (var i = 0; i < 3; i++) { |
上面代码片段中的匿名函数拥有独立的作用域,参数 j
是每轮遍历变量 i
值的一份拷贝,因此执行结果和问题 2中使用 ES6 的 let
关键字来声明循环变量的情形一致。
令人迷惑的 new
关联至问题 12
这个问题定义了一个函数,并在函数内部使用了 this
关键字。最关键的在于下方「调用」了两次这个函数,最重要的区别在于一个在调用之前使用了 new
关键字,而另外一个没有使用。
如果有一定的 JS 基础,对 this
关键字比较了解的话,很容易意识到,在没有使用 new
关键字的调用中,this
就会指向上一级对象,在这里就是全局对象,因此赋值过程并不会有任何问题,但是执行结果(返回值)是什么呢?
或许是因为这两种相似的用法摆在了一起,再加上受 Python 的实例化对象的语法的影响,我错误地认为返回值是 {}
,也就是一个空对象,但事实上这个没有使用 new
关键字的调用就是普通的函数调用,因为这个函数没有显式返回任何值,因此返回值就是 undefined
!
如果读者没有学习过 Python,或许就不会在这道问题上出错……在 Python 中无需使用
new
关键字,直接调用类构造器即可实例化一个相应的对象。
在使用了 new
关键字的调用中,this
就是指向新创建的空对象的,而所被调用的函数也摇身一变被称为「构造函数」,无论「构造函数」中是否返回值,实例化对象之后的返回值一定是这个创建对象的引用。在 ES6 之前,定义构造函数的写法就是如问题 12中这般简单,不得不让 C 系程序员感到十分新奇。而在 ES6 之后,也引入了 class
关键字,提供了其它面向对象语言类似的语法来定义类。
new.target
new.target
属性允许你检测函数或构造方法是否是通过 new
运算符被调用的。
- 在通过
new
运算符被初始化的函数或构造方法中,new.target
返回一个指向构造方法或函数的引用。 - 在普通的函数调用中,
new.target
的值是undefined
。
嗯,这是 MDN 上关于 new.target
的介绍,但是转念一想,这些功能基本可以通过 this
关键字来实现嘛,这又是 ES6 搞出的语法糖?不过由于这个语法的设计初衷就是为了判断构造方法是否是通过 new
运算符调用的,因此如果想要在继承后的子类的构造函数中的 super()
调用之前即执行相关判断会变得相当有用:
1 | class Parent { |
new.target
不能在构造函数或普通函数的外部使用
浏览器事件知识
关联至问题 13
当一个事件发生在具有父元素的元素上(例如,在我们的例子中是<video>元素)时,现代浏览器运行两个不同的阶段 - 捕获阶段和冒泡阶段。 在捕获阶段:
- 浏览器检查元素的最外层祖先<html>,是否在捕获阶段中注册了一个onclick事件处理程序,如果是,则运行它。
- 然后,它移动到<html>中单击元素的下一个祖先元素,并执行相同的操作,然后是单击元素再下一个祖先元素,依此类推,直到到达实际点击的元素。
在冒泡阶段,恰恰相反:
- 浏览器检查实际点击的元素是否在冒泡阶段中注册了一个onclick事件处理程序,如果是,则运行它
- 然后它移动到下一个直接的祖先元素,并做同样的事情,然后是下一个,等等,直到它到达<html>元素。