JavaScript 是 ECMAScript 规范的一种实现,本小节重点梳理下 ECMAScript 中的常考知识点,而后就一些容易出现的题目进行解析。前端
变量类型程序员
原型与原型链(继承)es6
做用域和闭包面试
异步json
ES6/7 新标准的考查数组
JavaScript 是一种弱类型脚本语言,所谓弱类型指的是定义变量时,不须要什么类型,在程序运行过程当中会自动判断类型。浏览器
ECMAScript 中定义了 6 种原始类型:网络
注意:原始类型不包含 Object。数据结构
题目:类型判断用到哪些方法?
typeof
typeof xxx
获得的值有如下几种类型:undefined
boolean
number
string
object
function
、symbol
,比较简单,再也不一一演示了。这里须要注意的有三点:闭包
typeof null
结果是object
,实际这是typeof
的一个bug,null是原始值,非引用类型typeof [1, 2]
结果是object
,结果中没有array
这一项,引用类型除了function
其余的所有都是object
typeof Symbol()
用typeof
获取symbol
类型的值获得的是symbol
,这是 ES6 新增的知识点instanceof
用于实例和构造函数的对应。例如判断一个变量是不是数组,使用typeof
没法判断,但可使用[1, 2] instanceof Array
来判断。由于,[1, 2]
是数组,它的构造函数就是Array
。同理:
function Foo(name) { this.name = name } var foo = new Foo('bar') console.log(foo instanceof Foo) // true
题目:值类型和引用类型的区别
除了原始类型,ES 还有引用类型,上文提到的typeof
识别出来的类型中,只有object
和function
是引用类型,其余都是值类型。
根据 JavaScript 中的变量类型传递方式,又分为值类型和引用类型,值类型变量包括 Boolean、String、Number、Undefined、Null,引用类型包括了 Object 类的全部,如 Date、Array、Function 等。在参数传递方式上,值类型是按值传递,引用类型是按共享传递。
下面经过一个小题目,来看下二者的主要区别,以及实际开发中须要注意的地方。
// 值类型 var a = 10 var b = a b = 20 console.log(a) // 10 console.log(b) // 20
上述代码中,a
b
都是值类型,二者分别修改赋值,相互之间没有任何影响。再看引用类型的例子:
// 引用类型 var a = {x: 10, y: 20} var b = a b.x = 100 b.y = 200 console.log(a) // {x: 100, y: 200} console.log(b) // {x: 100, y: 200}
上述代码中,a
b
都是引用类型。在执行了b = a
以后,修改b
的属性值,a
的也跟着变化。由于a
和b
都是引用类型,指向了同一个内存地址,即二者引用的是同一个值,所以b
修改属性时,a
的值随之改动。
再借助题目进一步讲解一下。
说出下面代码的执行结果,并分析其缘由。
function foo(a){ a = a * 10; } function bar(b){ b.value = 'new'; } var a = 1; var b = {value: 'old'}; foo(a); bar(b); console.log(a); // 1 console.log(b); // value: new
经过代码执行,会发现:
a
的值没有发生改变b
的值发生了改变这就是由于Number
类型的a
是按值传递的,而Object
类型的b
是按共享传递的。
JS 中这种设计的缘由是:按值传递的类型,复制一份存入栈内存,这类类型通常不占用太多内存,并且按值传递保证了其访问速度。按共享传递的类型,是复制其引用,而不是整个复制其值(C 语言中的指针),保证过大的对象等不会由于不停复制内容而形成内存的浪费。
引用类型常常会在代码中按照下面的写法使用,或者说容易不知不觉中形成错误!
var obj = { a: 1, b: [1,2,3] } var a = obj.a var b = obj.b a = 2 b.push(4) console.log(obj, a, b)
虽然obj
自己是个引用类型的变量(对象),可是内部的a
和b
一个是值类型一个是引用类型,a
的赋值不会改变obj.a
,可是b
的操做却会反映到obj
对象上。
JavaScript 是基于原型的语言,原型理解起来很是简单,但却特别重要,下面仍是经过题目来理解下JavaScript 的原型概念。
题目:如何理解 JavaScript 的原型
对于这个问题,能够从下面这几个要点来理解和回答,下面几条必须记住而且理解
null
除外)__proto__
属性,属性值是一个普通的对象prototype
属性,属性值也是一个普通的对象__proto__
属性值指向它的构造函数的prototype
属性值经过代码解释一下,你们可自行运行如下代码,看结果。
// 要点一:自由扩展属性 var obj = {}; obj.a = 100; var arr = []; arr.a = 100; function fn () {} fn.a = 100; // 要点二:__proto__ console.log(obj.__proto__); console.log(arr.__proto__); console.log(fn.__proto__); // 要点三:函数有 prototype console.log(fn.prototype) // 要点四:引用类型的 __proto__ 属性值指向它的构造函数的 prototype 属性值 console.log(obj.__proto__ === Object.prototype)
先写一个简单的代码示例。
// 构造函数 function Foo(name, age) { this.name = name } Foo.prototype.alertName = function () { alert(this.name) } // 建立示例 var f = new Foo('zhangsan') f.printName = function () { console.log(this.name) } // 测试 f.printName() f.alertName()
执行printName
时很好理解,可是执行alertName
时发生了什么?这里再记住一个重点 当试图获得一个对象的某个属性时,若是这个对象自己没有这个属性,那么会去它的__proto__
(即它的构造函数的prototype
)中寻找,所以f.alertName
就会找到Foo.prototype.alertName
。
那么如何判断这个属性是否是对象自己的属性呢?使用hasOwnProperty
,经常使用的地方是遍历一个对象的时候。
var item for (item in f) { // 高级浏览器已经在 for in 中屏蔽了来自原型的属性,可是这里建议你们仍是加上这个判断,保证程序的健壮性 if (f.hasOwnProperty(item)) { console.log(item) } }
题目:如何理解 JS 的原型链
仍是接着上面的示例,若是执行f.toString()
时,又发生了什么?
// 省略 N 行 // 测试 f.printName() f.alertName() f.toString()
由于f
自己没有toString()
,而且f.__proto__
(即Foo.prototype
)中也没有toString
。这个问题仍是得拿出刚才那句话——当试图获得一个对象的某个属性时,若是这个对象自己没有这个属性,那么会去它的__proto__
(即它的构造函数的prototype
)中寻找。
若是在f.__proto__
中没有找到toString
,那么就继续去f.__proto__.__proto__
中寻找,由于f.__proto__
就是一个普通的对象而已嘛!
f.__proto__
即Foo.prototype
,没有找到toString
,继续往上找f.__proto__.__proto__
即Foo.prototype.__proto__
。Foo.prototype
就是一个普通的对象,所以Foo.prototype.__proto__
就是Object.prototype
,在这里能够找到toString
f.toString
最终对应到了Object.prototype.toString
这样一直往上找,你会发现是一个链式的结构,因此叫作“原型链”。若是一直找到最上层都没有找到,那么就宣告失败,返回undefined
。最上层是什么 —— Object.prototype.__proto__ === null
this
全部从原型或更高级原型中获得、执行的方法,其中的this
在执行时,就指向了当前这个触发事件执行的对象。所以printName
和alertName
中的this
都是f
。
做用域和闭包是前端面试中,最可能考查的知识点。例以下面的题目:
题目:如今有个 HTML 片断,要求编写代码,点击编号为几的连接就
alert
弹出其编号
<ul> <li>编号1,点击我请弹出1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul>
通常不知道这个题目用闭包的话,会写出下面的代码:
var list = document.getElementsByTagName('li'); for (var i = 0; i < list.length; i++) { list[i].addEventListener('click', function(){ alert(i + 1) }, true) }
实际上执行才会发现始终弹出的是6
,这时候就应该经过闭包来解决:
var list = document.getElementsByTagName('li'); for (var i = 0; i < list.length; i++) { list[i].addEventListener('click', function(i){ return function(){ alert(i + 1) } }(i), true) }
要理解闭包,就须要咱们从「执行上下文」开始讲起。
先讲一个关于 变量提高 的知识点,面试中可能会碰见下面的问题,不少候选人都回答错误:
题目:说出下面执行的结果(这里笔者直接注释输出了)
console.log(a) // undefined var a = 100 fn('zhangsan') // 'zhangsan' 20 function fn(name) { age = 20 console.log(name, age) var age } console.log(b); // 这里报错 // Uncaught ReferenceError: b is not defined b = 100;
在一段 JS 脚本(即一个<script>
标签中)执行以前,要先解析代码(因此说 JS 是解释执行的脚本语言),解析的时候会先建立一个 全局执行上下文 环境,先把代码中即将执行的(内部函数的不算,由于你不知道函数什么时候执行)变量、函数声明都拿出来。变量先暂时赋值为undefined
,函数则先声明好可以使用。这一步作完了,而后再开始正式执行程序。再次强调,这是在代码执行以前才开始的工做。
咱们来看下上面的面试小题目,为何a
是undefined
,而b
却报错了,实际 JS 在代码执行以前,要「全文解析」,发现var a
,知道有个a
的变量,存入了执行上下文,而b
没有找到var
关键字,这时候没有在执行上下文提早「占位」,因此代码执行的时候,提早报到的a
是有记录的,只不过值暂时尚未赋值,即为undefined
,而b
在执行上下文没有找到,天然会报错(没有找到b
的引用)。
另外,一个函数在执行以前,也会建立一个 函数执行上下文 环境,跟 全局上下文 差很少,不过 函数执行上下文 中会多出this
arguments
和函数的参数。参数和arguments
好理解,这里的this
我们须要专门讲解。
总结一下:
<script>
、js 文件或者一个函数this
,arguments
this
先搞明白一个很重要的概念 —— this
的值是在执行的时候才能确认,定义的时候不能确认! 为何呢 —— 由于this
是执行上下文环境的一部分,而执行上下文须要在代码执行以前肯定,而不是定义的时候。看以下例子
var a = { name: 'A', fn: function () { console.log(this.name) } } a.fn() // this === a a.fn.call({name: 'B'}) // this === {name: 'B'} var fn1 = a.fn fn1() // this === window
this
执行会有不一样,主要集中在这几个场景中
a.fn()
fn1()
call
apply
bind
,上述代码中a.fn.call({name: 'B'})
下面再来说解下什么是做用域和做用域链,做用域链和做用域也是常考的题目。
题目:如何理解 JS 的做用域和做用域链
ES6 以前 JS 没有块级做用域。例如
if (true) { var name = 'zhangsan' } console.log(name)
从上面的例子能够体会到做用域的概念,做用域就是一个独立的地盘,让变量不会外泄、暴露出去。上面的name
就被暴露出去了,所以,JS 没有块级做用域,只有全局做用域和函数做用域。
var a = 100 function fn() { var a = 200 console.log('fn', a) } console.log('global', a) fn()
全局做用域就是最外层的做用域,若是咱们写了不少行 JS 代码,变量定义都没有用函数包括,那么它们就所有都在全局做用域中。这样的坏处就是很容易撞车、冲突。
// 张三写的代码中 var data = {a: 100} // 李四写的代码中 var data = {x: true}
这就是为什么 jQuery、Zepto 等库的源码,全部的代码都会放在(function(){....})()
中。由于放在里面的全部变量,都不会被外泄和暴露,不会污染到外面,不会对其余的库或者 JS 脚本形成影响。这是函数做用域的一个体现。
附:ES6 中开始加入了块级做用域,使用let
定义变量便可,以下:
if (true) { let name = 'zhangsan' } console.log(name) // 报错,由于let定义的name是在if这个块级做用域
首先认识一下什么叫作 自由变量 。以下代码中,console.log(a)
要获得a
变量,可是在当前的做用域中没有定义a
(可对比一下b
)。当前做用域没有定义的变量,这成为 自由变量 。自由变量如何获得 —— 向父级做用域寻找。
var a = 100 function fn() { var b = 200 console.log(a) console.log(b) } fn()
若是父级也没呢?再一层一层向上寻找,直到找到全局做用域仍是没找到,就宣布放弃。这种一层一层的关系,就是 做用域链 。
var a = 100 function F1() { var b = 200 function F2() { var c = 300 console.log(a) // 自由变量,顺做用域链向父做用域找 console.log(b) // 自由变量,顺做用域链向父做用域找 console.log(c) // 本做用域的变量 } F2() } F1()
讲完这些内容,咱们再来看一个例子,经过例子来理解闭包。
function F1() { var a = 100 return function () { console.log(a) } } var f1 = F1() var a = 200 f1()
自由变量将从做用域链中去寻找,可是 依据的是函数定义时的做用域链,而不是函数执行时,以上这个例子就是闭包。闭包主要有两个应用场景:
function F1() { var a = 100 return function () { console.log(a) } } function F2(f1) { var a = 200 console.log(f1()) } var f1 = F1() F2(f1)
至此,对应着「做用域和闭包」这部分一开始的点击弹出alert
的代码再看闭包,就很好理解了。
异步和同步也是面试中常考的内容,下面笔者来说解下同步和异步的区别。
先看下面的 demo,根据程序阅读起来表达的意思,应该是先打印100
,1秒钟以后打印200
,最后打印300
。可是实际运行根本不是那么回事。
console.log(100) setTimeout(function () { console.log(200) }, 1000) console.log(300)
再对比如下程序。先打印100
,再弹出200
(等待用户确认),最后打印300
。这个运行效果就符合预期要求。
console.log(100) alert(200) // 1秒钟以后点击确认 console.log(300)
这俩到底有何区别?—— 第一个示例中间的步骤根本没有阻塞接下来程序的运行,而第二个示例却阻塞了后面程序的运行。前面这种表现就叫作 异步(后面这个叫作 同步 ),即不会阻塞后面程序的运行。
JS 须要异步的根本缘由是 JS 是单线程运行的,即在同一时间只能作一件事,不能“一心二用”。
一个 Ajax 请求因为网络比较慢,请求须要 5 秒钟。若是是同步,这 5 秒钟页面就卡死在这里啥也干不了了。异步的话,就好不少了,5 秒等待就等待了,其余事情不耽误作,至于那 5 秒钟等待是网速太慢,不是由于 JS 的缘由。
讲到单线程,咱们再来看个真题:
题目:讲解下面代码的执行过程和结果
var a = true; setTimeout(function(){ a = false; }, 100) while(a){ console.log('while执行了') }
这是一个颇有迷惑性的题目,很多候选人认为100ms
以后,因为a
变成了false
,因此while
就停止了,实际不是这样,由于JS是单线程的,因此进入while
循环以后,没有「时间」(线程)去跑定时器了,因此这个代码跑起来是个死循环!
setTimeout
setInterval
Ajax
<img>
加载Ajax 代码示例
console.log('start') $.get('./data1.json', function (data1) { console.log(data1) }) console.log('end')
img 代码示例(经常使用于打点统计)
console.log('start') var img = document.createElement('img') // 或者 img = new Image() img.onload = function () { console.log('loaded') img.onload = null } img.src = '/xxx.png' console.log('end')
题目:ES6 箭头函数中的
this
和普通函数中的有什么不一样
箭头函数是 ES6 中新的函数定义形式,function name(arg1, arg2) {...}
可使用(arg1, arg2) => {...}
来定义。示例以下:
// JS 普通函数 var arr = [1, 2, 3] arr.map(function (item) { console.log(index) return item + 1 }) // ES6 箭头函数 const arr = [1, 2, 3] arr.map((item, index) => { console.log(index) return item + 1 })
箭头函数存在的意义,第一写起来更加简洁,第二能够解决 ES6 以前函数执行中this
是全局变量的问题,看以下代码
function fn() { console.log('real', this) // {a: 100} ,该做用域下的 this 的真实的值 var arr = [1, 2, 3] // 普通 JS arr.map(function (item) { console.log('js', this) // window 。普通函数,这里打印出来的是全局变量,使人费解 return item + 1 }) // 箭头函数 arr.map(item => { console.log('es6', this) // {a: 100} 。箭头函数,这里打印的就是父做用域的 this return item + 1 }) } fn.call({a: 100})
题目:ES6 模块化如何使用?
ES6 中模块化语法更加简洁,直接看示例。
若是只是输出一个惟一的对象,使用export default
便可,代码以下
// 建立 util1.js 文件,内容如 export default { a: 100 } // 建立 index.js 文件,内容如 import obj from './util1.js' console.log(obj)
若是想要输出许多个对象,就不能用default
了,且import
时候要加{...}
,代码以下
// 建立 util2.js 文件,内容如 export function fn1() { alert('fn1') } export function fn2() { alert('fn2') } // 建立 index.js 文件,内容如 import { fn1, fn2 } from './util2.js' fn1() fn2()
题目:ES6 class 和普通构造函数的区别
class 其实一直是 JS 的关键字(保留字),可是一直没有正式使用,直到 ES6 。 ES6 的 class 就是取代以前构造函数初始化对象的形式,从语法上更加符合面向对象的写法。例如:
JS 构造函数的写法
function MathHandle(x, y) { this.x = x; this.y = y; } MathHandle.prototype.add = function () { return this.x + this.y; }; var m = new MathHandle(1, 2); console.log(m.add())
用 ES6 class 的写法
class MathHandle { constructor(x, y) { this.x = x; this.y = y; } add() { return this.x + this.y; } } const m = new MathHandle(1, 2); console.log(m.add())
注意如下几点,全都是关于 class 语法的:
class Name {...}
这种形式,和函数的写法彻底不同constructor
函数中,constructor
即构造器,初始化实例时默认执行add() {...}
这种形式,并无function
关键字使用 class 来实现继承就更加简单了,至少比构造函数实现继承简单不少。看下面例子
JS 构造函数实现继承
// 动物 function Animal() { this.eat = function () { console.log('animal eat') } } // 狗 function Dog() { this.bark = function () { console.log('dog bark') } } Dog.prototype = new Animal() // 哈士奇 var hashiqi = new Dog()
ES6 class 实现继承
class Animal { constructor(name) { this.name = name } eat() { console.log(`${this.name} eat`) } } class Dog extends Animal { constructor(name) { super(name) this.name = name } say() { console.log(`${this.name} say`) } } const dog = new Dog('哈士奇') dog.say() dog.eat()
注意如下两点:
extends
便可实现继承,更加符合经典面向对象语言的写法,如 Javaconstructor
必定要执行super()
,以调用父类的constructor
题目:ES6 中新增的数据类型有哪些?
Set 和 Map 都是 ES6 中新增的数据结构,是对当前 JS 数组和对象这两种重要数据结构的扩展。因为是新增的数据结构,目前还没有被大规模使用,可是做为前端程序员,提早了解是必须作到的。先总结一下二者最关键的地方:
Set
Set 实例不容许元素有重复,能够经过如下示例证实。能够经过一个数组初始化一个 Set 实例,或者经过add
添加元素,元素不能重复,重复的会被忽略。
// 例1 const set = new Set([1, 2, 3, 4, 4]); console.log(set) // Set(4) {1, 2, 3, 4} // 例2 const set = new Set(); [2, 3, 5, 4, 5, 8, 8].forEach(item => set.add(item)); for (let item of set) { console.log(item); } // 2 3 5 4 8
Set 实例的属性和方法有
size
:获取元素数量。add(value)
:添加元素,返回 Set 实例自己。delete(value)
:删除元素,返回一个布尔值,表示删除是否成功。has(value)
:返回一个布尔值,表示该值是不是 Set 实例的元素。clear()
:清除全部元素,没有返回值。const s = new Set(); s.add(1).add(2).add(2); // 添加元素 s.size // 2 s.has(1) // true s.has(2) // true s.has(3) // false s.delete(2); s.has(2) // false s.clear(); console.log(s); // Set(0) {}
Set 实例的遍历,可以使用以下方法
keys()
:返回键名的遍历器。values()
:返回键值的遍历器。不过因为 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),因此keys()
和values()
返回结果一致。entries()
:返回键值对的遍历器。forEach()
:使用回调函数遍历每一个成员。let set = new Set(['aaa', 'bbb', 'ccc']); for (let item of set.keys()) { console.log(item); } // aaa // bbb // ccc for (let item of set.values()) { console.log(item); } // aaa // bbb // ccc for (let item of set.entries()) { console.log(item); } // ["aaa", "aaa"] // ["bbb", "bbb"] // ["ccc", "ccc"] set.forEach((value, key) => console.log(key + ' : ' + value)) // aaa : aaa // bbb : bbb // ccc : ccc
Map
Map 的用法和普通对象基本一致,先看一下它能用非字符串或者数字做为 key 的特性。
const map = new Map(); const obj = {p: 'Hello World'}; map.set(obj, 'OK') map.get(obj) // "OK" map.has(obj) // true map.delete(obj) // true map.has(obj) // false
须要使用new Map()
初始化一个实例,下面代码中set
get
has
delete
顾名便可思义(下文也会演示)。其中,map.set(obj, 'OK')
就是用对象做为的 key (不光能够是对象,任何数据类型均可以),而且后面经过map.get(obj)
正确获取了。
Map 实例的属性和方法以下:
size
:获取成员的数量set
:设置成员 key 和 valueget
:获取成员属性值has
:判断成员是否存在delete
:删除成员clear
:清空全部const map = new Map(); map.set('aaa', 100); map.set('bbb', 200); map.size // 2 map.get('aaa') // 100 map.has('aaa') // true map.delete('aaa') map.has('aaa') // false map.clear()
Map 实例的遍历方法有:
keys()
:返回键名的遍历器。values()
:返回键值的遍历器。entries()
:返回全部成员的遍历器。forEach()
:遍历 Map 的全部成员。const map = new Map(); map.set('aaa', 100); map.set('bbb', 200); for (let key of map.keys()) { console.log(key); } // "aaa" // "bbb" for (let value of map.values()) { console.log(value); } // 100 // 200 for (let item of map.entries()) { console.log(item[0], item[1]); } // aaa 100 // bbb 200 // 或者 for (let [key, value] of map.entries()) { console.log(key, value); } // aaa 100 // bbb 200
Promise
是 CommonJS 提出来的这一种规范,有多个版本,在 ES6 当中已经归入规范,原生支持 Promise 对象,非 ES6 环境能够用相似 Bluebird、Q 这类库来支持。
Promise
能够将回调变成链式调用写法,流程更加清晰,代码更加优雅。
简单概括下 Promise:三个状态、两个过程、一个方法,快速记忆方法:3-2-1
三个状态:pending
、fulfilled
、rejected
两个过程:
一个方法:then
固然还有其余概念,如catch
、 Promise.all/race
,这里就不展开了。
关于 ES6/7 的考查内容还有不少,本小节就不逐一介绍了,若是想继续深刻学习,能够在线看《ES6入门》。
本小节主要总结了 ES 基础语法中面试常常考查的知识点,包括以前就考查较多的原型、异步、做用域,以及 ES6 的一些新内容,这些知识点但愿你们都要掌握。