作为面试官,我们经常考察候选人的 JavaScript 基础,从而决定是否录用这个人。那么对于一个开发者,哪些东西算JavaScript基础,什么叫做基础好呢?
注意:这里不会关注 API 或者语法层面的东西。这并不代表 API 或者语法不重要,相反,作为 JavaScript 开发者,最基本的能力就是能跟进到最新的 ES 规范。
数据类型
JS 中数据类型主要分为基本类型和引用类型(也就是对象)。这里重点关注一下对象。
对象和原型
先来一段代码
function Person() {
}
// 虽然写在注释里,但是你要注意:
// prototype是函数才会有的属性
Person.prototype.name = 'Kevin';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // Kevin
console.log(person2.name) // Kevin
代码中 Persion 就是一个构造函数,我们可以通过构造函数生成多个对象。这些对象共享一个原型,也就是构造函数的 prototype
。JS 中大多数对象(除了 null
、object.crate(null)
等 )都有一个原型。原型也只是个普通对象,
因此它也有自己的原型,原型与原型之间组成原型链,像链表一样,原型链最终指向 null
。
当访问对象的属性时,如果在它自身上找不到,那么就去它的原型上找;如果再找不到,就去它的原型的原型上找。。。最终找到 null
,就返回 undefined
。
优点:因为多个对象共享一个原型,原型可以节省很多内存,并且非常灵活。
缺点:共享也会引起污染,所以不要轻易更改原型上的属性。另外,原型链的查找和链表一样,也是比较耗时的。
函数参数传递
函数参数分为基本类型和对象,基本类型统一按值传递,对象的传递比较复杂,分为两种情况:
- 当我们直接修改整个对象的引用时,按照值传递的规则,不会影响原本的对象。
- 当我们修改对象上的某个属性时,会直接更改原对象。
比如 Commonjs 模块,我们之所以可以访问到 module.exports
exports
等变量,是因为整个模块执行时被包裹在一个函数里,这些变量作为参数传入。例如:
function moduleResolver(module, exports) {
// 可以导出
module.exports = { a: 1 };
module.exports.a = 1;
exports.a = 1;
// 不可以导出
exports = { a: 1 };
}
最后一种状况我们整个更改了 exports 的引用,不会影响到原对象,因此也无法导出这个变量。
数据类型判断和转换
JS 是弱类型的,并且会有很多历史坑点和隐式类型转换,比如网上传的比较火的 JS 真假值表。这里我们重点关注常见的基本类型转换和基本类型到引用类型的转换。
基本类型:比如数字和布尔值,字符和布尔值的相互转换,null,undefined等
基本类型到引用类型:比如基本类型上直接调用其构造函数原型上的方法,会把其隐式转换为对应的引用类型。比如
var p = 'The quick brown fox jumps over the lazy dog. If the dog reacted, was it really lazy?';
var regex = /dog/gi;
console.log(p.replace(regex, 'ferret'));
执行上下文
JS 中执行上下文有两类:全局上下文和函数上下文(eval 暂时不谈),一个上下文主要包括两部分:作用域和 This。JS 中通过栈来管理执行上下文,函数的执行和退出分别会入栈和出栈。
作用域
JavaScript 中采用词法作用域,也就是说变量的作用域是定义而非执行的时候决定的。当我们访问变量时,如果在当前作用域找不到,就会去父级作用域查找,直到全局作用域。这就是作用域链。
当我们访问非当前作用域的变量时,就会产生一个闭包,闭包会保护被访问的变量不被垃圾回收机制回收。使用得当的话,闭包可以实现很多功能。比如 debounce
,代码如下:
function debounce(fn, timeout) {
let timer = null;
return function (...args) {
const ctx = this;
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.call(ctx, ...args)
timer = null;
}, timeout);
}
}
这里就用闭包保存了一个定时器,在函数重复调用产生防抖效果。
闭包也会产生一些意料之外的效果,比如 react 中的 hooks 陷阱。
This
不同于作用域,this 是在代码执行时决定的,分为以下几种情况:
- 全局上下文中 this,非严格模式下指向 window 对象
- 对象访问 this ,指向调用的对象。
- call、apply、bind 调用,指向传入的第一个参数
- 箭头函数,箭头函数没有自己的this, 它会从父级上下文继承 this
- 构造函数、类,执行新建的实例。
模块化
JS 中现在使用的比较多的模块规范是 CommonJS 和 ES Module。
CommonJS
CommonJs 是 Nodejs 中广泛采用的一种模块规范,它有以下特点:
- 同步导入,因为模块都在本地磁盘中。
- 动态导入,可以在 if for 语句中导入模块。
ES Module
CommonJs 虽然在 Node 中广受欢迎,但是浏览器中模块需要从服务器加载,也就是异步的,ES 为此提出了自己的模块规范: ES Module。
ES Module 中只能在模块顶层导入其他模块,无法动态引入,这使得模块编译时是确定的,基于此的模块静态解析可以实现 webpack 中 tree shaking 的功能。
运行环境
ES 提出了自己的语言规范,不同的宿主环境实现了这个规范,使得 js 代码可以运行在不同的环境中。目前比较常见的运行环境是 NodeJs 和浏览器。
事件循环
众所周知,JS 是单线程的,为了管理异步代码,JS 中引入了事件循环的概念。事件循环中我们把异步任务分为两种:
- 微任务 包括 Promise.then Promise.catch MutationObserver 等
- 宏任务 包括 setTimeout setInterval
异步执行流程如下:
每次同步代码执行结束,会按照以下执行异步代码:
- 取出一个宏任务执行,执行结束
- 检查微任务队列,如果有,执行所有微任务
- 浏览器渲染,重复步骤1
哪些是可选的
9102年了,有些技术或者概念的确是落伍或者说不那么重要了,比如上面提到的 原型 、 变量提升、函数提升、==,IE下的兼容问题等。主要有以下两个原因:
- 实际开发中几乎用不到
- 我相信你学习计算机基础,比如算法的收益会更大