变量类型
JavaScript 是一种弱类型脚本语言, 所谓弱类型指的是定义变量时, 不需要什么类型, 在程序运行过程中会自动判断类型
ECMAScript 中定义了 6 种原始类型:
- Boolean
- String
- Number
- Null
- Undefined
- Symbol(ES6 新定义)
注意: 原始类型不包含 Object
第一问: 类型判断用到哪些方法?
typeof
typeof xxx 得到的值有以下几种类型: undefinedbooleannumberstringobjectfunctionsymbol , 比较简单这里需要注意的有三点:
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
第二问: 值类型和引用类型的区别
值类型 vs 引用类型
除了原始类型, ES 还有引用类型, 上文提到的 typeof 识别出来的类型中, 只有 object 和 function 是引用类型, 其他都是值类型
根据 JavaScript 中的变量类型传递方式, 又分为值类型和引用类型, 值类型变量包括 BooleanStringNumberUndefinedNull, 引用类型包括了 Object 类的所有, 如 DateArrayFunction 等在参数传递方式上, 值类型是按值传递, 引用类型是按共享传递
面通过一个小题目, 来看下两者的主要区别, 以及实际开发中需要注意的地方
- // 值类型
- 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__就是一个普通的对象而已嘛!
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}
这就是为何 jQueryZepto 等库的源码, 所有的代码都会放在 (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 的代码再看闭包, 就很好理解了
来源: http://www.bubuko.com/infodetail-2520780.html