JavaScript 是一种弱类型脚本语言,所谓弱类型指的是定义变量时,不须要什么类型,在程序运行过程当中会自动判断类型。html
ECMAScript 中定义了 6 种原始类型:前端
注意:原始类型不包含 Object。面试
第一问:类型判断用到哪些方法?数组
typeof
typeof xxx
获得的值有如下几种类型:undefined、
boolean、
number、s
tring、
object、
function
、symbol
,比较简单。这里须要注意的有三点:浏览器
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
第二问:值类型和引用类型的区别app
除了原始类型,ES 还有引用类型,上文提到的typeof
识别出来的类型中,只有object
和function
是引用类型,其余都是值类型。函数
根据 JavaScript 中的变量类型传递方式,又分为值类型和引用类型,值类型变量包括 Boolean、String、Number、Undefined、Null,引用类型包括了 Object 类的全部,如 Date、Array、Function 等。在参数传递方式上,值类型是按值传递,引用类型是按共享传递。测试
面经过一个小题目,来看下二者的主要区别,以及实际开发中须要注意的地方。this
// 值类型
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)//{a:1,b:[1,2,3,4]},2,[1,2,3,4]
虽然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__
就是一个普通的对象而已嘛!
这样一直往上找,你会发现是一个链式的结构,因此叫作“原型链”。若是一直找到最上层都没有找到,那么就宣告失败,返回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() //100
自由变量将从做用域链中去寻找,可是 依据的是函数定义时的做用域链,而不是函数执行时,以上这个例子就是闭包。闭包主要有两个应用场景:
function F1() { var a = 100
return function () { console.log(a) } } function F2(f1) { var a = 200 console.log(f1()) }
var f1 = F1() F2(f1) //100
至此,对应着「做用域和闭包」这部分一开始的点击弹出alert
的代码再看闭包,就很好理解了。