面试—JavaScript

135 阅读25分钟

tvar const let

var ——ES5 变量声明方式

  1. 在变量未赋值时,变量undefined(为使用声明变量时也为undefined)
  2. 作用域——var的作用域为方法作用域只要在方法内定义了,整个方法内的定义变量后的代码都可以使用
  3. 函数内部如果用 var 声明了相同名称的外部变量,函数将不再向上寻找

let——ES6变量声明方式

  1. 在变量为声明前直接使用会报错
  2. 作用域——let为块作用域——通常let比var 范围要小
  3. let禁止重复声明变量,否则会报错;var可以重复声明

const——ES6变量声明方式

  1. const为常量声明方式;声明变量时必须初始化,在后面出现的代码中不能再修改该常量的值
  2. const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址不得改动

介绍一下js的数据类型

]基本数据类型:

Number,String,Boolean,null,undefined,

Symbol(表示独一无二的值)、

bigInt(表示范围比 NUmber 更大的整数,用于解决整数溢出的问题)(后两个为ES6新增)

引用数据类型:

object,function(proto Function.prototype)

object:普通对象,数组对象,正则对象,日期对象,Math数学函数对象。

两种数据类型的区别:

基本数据类型是直接存储在中的简单数据段,占据空间小、大小固定,属于被频繁使用的数据。栈是存储基 本类型值和执行代码的空间。

引用数据类型是存储在内存中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆 中该实体的起始地址,当解释器寻找引用值时,会检索其在栈中的地址,取得地址后从堆中获得实体。

js 类型转换

juejin.cn/post/684490…

显示类型转换

  • 转化为 Number 类型:Number() / parseFloat() / parseInt()
  • 转化为 String 类型:String() / toString()
  • 转化为 Boolean 类型: Boolean()

隐式类型转换

JS 的隐式转换主要涉及的是两个操作符, +==

map 和 forEach 的区别

参考答案:

相同点:

  1. 都是循环遍历数组中的每一项
  2. 每次执行匿名函数都支持三个参数,参数分别为item(当前每一项),index(索引值),arr(原数组)
  3. 匿名函数中的this都是指向window
  4. 只能遍历数组

不同点:

  1. map()会分配内存空间存储新数组并返回,forEach()不会返回数据。
  2. forEach()允许callback更改原始数组的元素。map()返回新的数组。

for...in 迭代和 for...of 有什么区别

1、 推荐在循环对象属性的时候,使用 for...in,在遍历数组的时候的时候使用for...of。

2、 for in遍历的是数组的索引,而for of遍历的是数组元素值

3、for...of 不能循环普通的对象,需要通过和 Object.keys()搭配使用

4、for...in 便利顺序以数字为先 无法便利 symbol 属性 可以便利到公有中可枚举的

5、从遍历对象的角度来说,for···in会遍历出来的为对象的key,但for···of会直接报错。

区别:

  1. for in 遍历的是对象的属性名(key) ,for of 遍历的是对象的元素值(value) ,所以用for in 来遍历对象
  2. 用for of 来遍历数组可以保证顺序,以及实现了iterator接口的对象,遍历普通对象会报错

for…of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)

for … in是为遍历对象属性而构建的

什么是深拷贝,浅拷贝,浅拷贝赋值的区别,如何实现

深拷贝和浅拷贝是针对复杂数据类型来说的,浅拷贝只拷贝一层,而深拷贝是层层拷贝

1.浅拷贝:

将原对象或原数组的地址直接赋给新对象,新数组,新对象只是对原对象的一个引用,而不复制对象本身,新旧对象还是共享同一块内存

如果属性是一个基本数据类型,拷贝就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,

2.深拷贝:

创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”

深拷贝就是把一个对象,从内存中完整的拷贝出来,从【堆内存】中开辟了新区域,用来存拷贝过来的新对象,并且修改新对象不会影响原对象

const arr = [2,1,3]
​
let q = arr.slice() // 浅拷贝let s = JSON.parse(JSON.stringify(arr)) // 深拷贝console.log(q, s);

3、赋值:

当我们把一个对象赋值给一个新的变量时,赋的是该对象在【栈中的内存地址】 ,而不是【堆中的数据】 。也就是两个对象

数组方法

1、sort( ) : sort 排序 如果下面参数的正反 控制 升序和降序 ,返回的是从新排序的原数组

2、splice( ): 向数组的指定index处插入 返回的是被删除掉的元素的集合,会改变原有数组;截取类 没有参 数,返回空数组,原数组不变;一个参数,从该参数表示的索引位开始截取,直至数组结束,返回截取的 数组,原数组改变;两个参数,第一个参数表示开始截取的索引位,第二个参数表示截取的长度,返回截取的 数组,原数组改变;三个或者更多参数,第三个及以后的参数表示要从截取位插入的值。会改变原数据

3、pop( ): 从尾部删除一个元素 返回被删除掉的元素,改变原有数组。

4、push( ):向数组的末尾追加 返回值是添加数据后数组的新长度,改变原有数组。

5、unshift( ):向数组的开头添加 返回值是添加数据后数组的新长度,改变原有数组。

6、shift( ):从头部删除一个元素 返回被删除掉的元素,改变原有数组。

7、reverse( ): 原数组倒序 它的返回值是倒序之后的原数组

8、concat( ):数组合并

9、slice( ) :数组元素的截取,返回一个新数组,新数组是截取的元素,可以为负值。从数组中截取,如果不传参,会返回原数组。如果只传入一个参数,会从头部开始删除,直到数组结束,原数组不会改变;传入两个参数,第一个是开始截取的索引,第二个是结束截取的索引,不包含结束截取的这一项,原数组不会改变。最多可以接受两个参数。

blog.csdn.net/z591102/art…

10、join( ):将数组进行分割成为字符串 这能分割一层在套一层就分隔不了了

11、toString( ):数组转字符串

12、toLocaleString( ) :将数组转换为本地数组。

13、forEach( ) :数组进行遍历;

forEach方法中的function回调有三个参数: 第一个参数是遍历的数组内容, 第二个参数是对应的数组索引, 第三个参数是数组本身

14、map( ) :没有return时,对数组的遍历。有return时,返回一个新数组,该新数组的元素是经过过滤(逻辑处理)过的函数。

15、filter( ): 对数组中的每一运行给定的函数,会返回满足该函数的项组成的数组。

16、every( ) :当数组中每一个元素在callback上被返回true时就返回true。(注:every其实类似filter,只不过它的功能是判断是不是数组中的所有元素都符合条件,并且返回的是布尔值)。

17、some( ) :当数组中有一个元素在callback上被返回true时就返回true。(注:every其实类似filter,只不过它的功能是判断是不是数组中的所有元素都符合条件,并且返回的是布尔值)。

15、16、17 三者的区别

blog.csdn.net/vhgvhbj/art…

18、reduce( ) :回调函数中有4个参数。prev(之前计算过的值),next(之前计算过的下一个的值),index,arr。把数组列表计算成一个

19、isArray() 判断是否是数组

20、indexOf 找索如果找到了就会返回当前的一个下标,若果没找到就会反回-1

21、lastIndexOf 它是从最后一个值向前查找的 找索如果找到了就会返回当前的一个下标,若果没找到就会反回-1

22、Array.of() 填充单个值

23、Array.from() ES6为Array增加了from函数用来将其他对象转换成数组

blog.csdn.net/wxl1555/art…

blog.csdn.net/wxl1555/art…

24、fill(): 填充方法 可以传入3各参数 可以填充数组里的值也就是替换 如果一个值全部都替换掉 , 第一个参数就是值 第二个参数 从起始第几个 第三个参数就是最后一个 find 查找这一组数 符合条件的第一个数 给他返回出来 25、findIndex() 查找这一组数 符合条件的第一数的下标 给他返回出来 没有返回 -1 keys 属性名 values属性值 entries属性和属性值 forEach 循环便利 有3个参数 无法使用 break continue , 参数一就是每个元素 参数二就是每个下标 参数三就是每个一项包扩下标和元素

字符串方法

  1. match() 方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。

该方法类似 indexOf() 和 lastIndexOf(),但是它返回结果数组,而不是字符串的位置。

blog.csdn.net/weixin_4336…

  1. replace()

    该方法 返回一个新的字符串,但并不改变字符串本身。

    该方法接收2个参数, 第一个参数可以是字符串,也可以是一个正则表达式; 第二个参数可以是一个字符串,也可以是一个函数。

    blog.csdn.net/qq_46658751…

防抖和节流

# 7分钟理解JS的节流、防抖及使用场景

防抖:所谓防抖,就是指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。相当于回城

考虑⼀个场景,有⼀个按钮点击会触发⽹络请求,但是我们并不希望每次点击都发起⽹络请求,⽽是当⽤户点击按钮⼀段时间后没有再次点击的情况才去发 起⽹络请求,对于这种情况我们就可以使⽤防抖。

节流:所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。相当于技能CD

考虑⼀个场景,滚动事件中会发起⽹络请求,但是我们并不希望⽤户在滚动过程中⼀直发起请求,⽽是隔⼀段时间发起⼀次,对于这种情况我们就可以使⽤节流。

预加载、预渲染

24.6 预加载

  • 在开发中,可能会遇到这样的情况。有些资源不需要马上⽤到,但是希望尽早获取,这时候就可以使⽤预加载。
  • 预加载其实是声明式的 fetch ,强制浏览器请求资源,并且不会阻塞 onload 事件,可以使⽤以下代码开启预加载
<link rel="preload" href="http://blog.poetries.top">
  • 预加载可以⼀定程度上降低⾸屏的加载时间,因为可以将⼀些不影响⾸屏但重要的⽂件延后加载,
  • 唯⼀缺点就是兼容性不好

24.7 预渲染

  • 可以通过预渲染将下载的⽂件预先在后台渲染,可以使⽤以下代码开启预渲染
<link rel="prerender" href="http://blog.poetries.top">
  • 预渲染虽然可以提⾼⻚⾯的加载速度,但是要确保该⻚⾯⼤概率会被⽤户在之后打开,否则就是⽩⽩浪费资源去渲染。

懒执行、懒加载

24.8 懒执⾏

  • 懒执⾏就是将某些逻辑延迟到使⽤时再计算
  • 该技术可以⽤于⾸屏优化,对于 某些耗时逻辑并不需要在⾸屏就使⽤的,就可以使⽤懒执⾏。懒执⾏需要唤醒,⼀般可以通过定时器或者事件的调⽤来唤醒。

24.9 懒加载

  • 懒加载就是将不关键的资源延后加载
  • 懒加载的原理就是只加载⾃定义区域(通常是可视区域,但也可以是即将进⼊可视区域) 内需要加载的东⻄。对于图⽚来说,先设置图⽚标签的 src 属性为⼀张占位图,将真实 的图⽚资源放⼊⼀个⾃定义属性中,当进⼊⾃定义区域时,就将⾃定义属性替换为 src 属性,这样图⽚就会去下载资源,实现了图⽚懒加载。
  • 懒加载不仅可以⽤于图⽚,也可以使⽤在别的资源上。⽐如进⼊可视区域才开始播放视频等等。

CDN的原理

24.10 CDN

  • CDN 的原理是尽可能的在各个地⽅分布机房缓存数据,这样即使我们的根服务器远在国外,在国内的⽤户也可以通过国内的机房迅速加载资源。
  • 因此,我们可以将静态资源尽量使⽤ CDN 加载,由于浏览器对于单个域名有并发请求上限,可以考虑使⽤多个 CDN 域名。
  • 并且对于 CDN 加载静态资源 需要注意 CDN 域名要与主站不同,否则每次请求都会带上主站的 Cookie ,平白消耗流量

堆和栈存储机制有什么区别

堆 是一种非连续的树形储存数据结构,每个节点有一个值,整棵树是经过排序的。特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。常用来实现优先队列,存取随意。

栈 是一种连续储存的数据结构,具有先进后出的性质。 通常的操作有入栈(压栈),出栈和栈顶元素。想要读取栈中的某个元素,就是将其之间的所有元素出栈才能完成。

两种数据存储方式的区别:

  1. 堆比栈空间大,栈比堆运行速度快
  2. 堆内存是无序存储,可以根据引用直接获取。
  3. 基础数据类型比较稳定,而且相对来说占用的内存小。
  4. 引用数据类型大小是动态的,而且是无限的。

什么是事件

事件是文档和浏览器窗口中发生的特定的交互瞬间,事件就发生了。

一、是直接在标签内直接添加执行语句,

二、是定义执行函数。

DOM事件分为两种类型:事件捕获、事件冒泡。

事件捕获就是:网景公司提出的事件流叫事件捕获流,由外往内,从事件发生的顶点开始,逐级往下查找,一直到目标元素。

事件冒泡:IE提出的事件流叫做事件冒泡就是由内往外,从具体的目标节点元素触发,逐级向上传递,直到根节点。

什么是事件流?

事件流就是,页面接受事件的先后顺序就形成了事件流。

自定义事件,就是自己定义事件类型,自己定义事件处理函数。

什么是事件委托

事件委托,又名事件代理。事件委托就是利用事件冒泡,就是把子元素的事件都绑定到父元素上。如果子元素阻止了事件冒泡,那么委托也就没法实现了

阻止事件冒泡 event.stopPropagation()

好处:提高性能,减少了事件绑定,从而减少内存占用

原生JS之DOM操作

原生JS之DOM操作

new原理

new实际上是在堆内存中开辟一个空间。

  • 创建一个空对象
  • 将构造函数中的this指向这个空对象;
  • 将新对象链接到原型;
  • 执行构造函数,将属性和方法添加到this指向的对象中;
  • 返回新对象

typeof 和 instanceof 的原理

JS数据类型之问——检测篇

instanceof 可以正确的判断对象的类型,因为内部机制是 查找构造函数的原型对象 是否在 实例对象的原型链上

如果 a instanceof B ,那么 a 必须要是个对象,而 B 必须是一个合法的函数。在这两个条件都满足的情况下:判断 B 的 prototype 属性指向的原型对象B.prototype )是否在对象 a原型链上。

var b=String("123");
​
b instanceof String;
​
//true

实现⼀下 instanceof

  • ⾸先获取类型的原型
  • 然后获得对象的原型
  • 然后⼀直循环判断对象的原型是否等于类型的原型,直到对象原型为 null ,因为原型链最终为 null

typeof 对于基本类型来说,除了 null 都可以显示正确的类型

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'

typeof 对于对象来说,除了函数都会显示 object ,所以说 typeof 并 不能准确判断变量到底是什么类型

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

如果我们想判断⼀个对象的正确类型,这时候可以考虑使⽤ instanceof , 因为内部机制是通过原型链来判断的

为什么 0.1 + 0.2 !== 0.3,如何让其相等

// 在开发过程中遇到类似这样的问题:
  let n1 = 0.1;
  let n2 = 0.2;
  console.log(n1 + n2)  // 0.30000000000000004
  
  /**
   * 使用浮点数进行计算逻辑处理时,不注意,就可能出现问题
   * 记住,永远不要直接比较俩个浮点的大小
   * 这个属于数字运算中的精度缺失的问题
   * 在0.1 + 0.2这个式子中,0.1和0.2都是近似表示的,在他们相加的时候,两个近似值进行了计算,导致最后得到的值是0.30000000000000004
  */
  // 简单粗暴的方式
  parseFloat((0.1 + 0.2).toFixed(10)) === 0.3
  // 最简单的方式
  (0.1 + 0.2)*10/10 === 0.3

我们可以发现, 0.1 在⼆进制中是⽆限循环的⼀些数字,其实不只是 0.1 ,其实很多⼗进制⼩数⽤⼆进制表示都是⽆限循环的。这样其实没什么 问题,但是 JS 采⽤的浮点数标准却会裁剪掉我们的数字

require与import的区别

1、import是ES6中的语法标准也是用来加载模块文件的,import函数可以读取并执行一个JavaScript文件,然后返回该模块的export命令指定输出的代码。export与export default均可用于导出常量、函数、文件、模块,export可以有多个,export default只能有一个。

2、require 定义模块:module变量代表当前模块,它的exports属性是对外的接口。通过exports可以将模块从模块中导出,其他文件加载该模块实际上就是读取module.exports变量,他们可以是变量、函数、对象等。在node中如果用exports进行导出的话系统会系统帮您转成module.exports的,只是导出需要定义导出名。

1、require是CommonJS规范的模块化语法,import是ES6规范的模块化语法;

2、require是运行时加载,import是编译时加载

3、require可以写在代码的任意位置,import只能写在文件的最顶端且不可在条件语句或函数作用域中使用

4、require通过module.exports导出的值就不能再变化,import通过export导出的值可以改变

5、require通过module.exports导出的是exports对象,import通过export导出是指定输出的代码

6、require运行时才引入模块的属性所以性能相对较低,import编译时引入模块的属性所以性能稍高。

set 和 map 常用的属性和方法

  // 1.Set
  const set1 = new Set()

  // 增加元素 使用 add
  set2.add(4)
  
  // 是否含有某个元素 使用 has
  console.log(set2.has(2)) 
  
  // 查看长度 使用 size
  console.log(set2.size) 
  
  // 删除元素 使用 delete
  set2.delete(2)
  
  // size: 返回Set实例的成员总数。
  // add(value):添加某个值,返回 Set 结构本身。
  // delete(value):删除某个值。
  // clear():清除所有成员,没有返回值。






  // 2.Map
   
    // 定义map
    const map1 = new Map()
    
    // 新增键值对 使用 set(key, value)
    map1.set(true, 1)
    
    // 判断map是否含有某个key 使用 has(key)
    console.log(map1.has('哈哈')) 
    
    // 获取map中某个key对应的value
    console.log(map1.get(true)) 
    
    // 删除map中某个键值对 使用 delete(key)
    map1.delete('哈哈')
    
    
    // 定义map,也可传入键值对数组集合
    const map2 = new Map([[true, 1], [1, 2], ['哈哈', '嘻嘻嘻']])
    console.log(map2) // Map(3) { true => 1, 1 => 2, '哈哈' => '嘻嘻嘻' }

null 和 undefined 的区别

null 和 undefined 的区别,如何让一个属性变为null

参考答案:

undefined 表示一个变量自然的、最原始的状态值,而 null 则表示一个变量被人为的设置为空对象,而不是原始状态。所以,在实际使用过程中,为了保证变量所代表的语义,不要对一个变量显式的赋值 undefined,当需要释放一个对象时,直接赋值为 null 即可。

解析:

undefined 的字面意思就是:未定义的值 。这个值的语义是,希望表示一个变量最原始的状态,而非人为操作的结果 。 这种原始状态会在以下 4 种场景中出现:

  1. 声明了一个变量,但没有赋值
  2. 访问对象上不存在的属性
  3. 函数定义了形参,但没有传递实参
  4. 使用 void 对表达式求值

因此,undefined 一般都来自于某个表达式最原始的状态值,不是人为操作的结果。当然,你也可以手动给一个变量赋值 undefined,但这样做没有意义,因为一个变量不赋值就是 undefined 。

null 的字面意思是:空值 。这个值的语义是,希望表示 一个对象被人为的重置为空对象,而非一个变量最原始的状态 。 在内存里的表示就是,栈中的变量没有指向堆中的内存对象

img

null 有属于自己的类型 Null,而不属于Object类型,typeof 之所以会判定为 Object 类型,是因为JavaScript 数据类型在底层都是以二进制的形式表示的,二进制的前三位为 0 会被 typeof 判断为对象类型,而 null 的二进制位恰好都是 0 ,因此,null 被误判断为 Object 类型

浅析apply、bind、call

浅解析call、apply 和 bind

image.png

call、apply 和 bind 是挂在 Function 对象上的三个方法,所以调用这三个方法的必须是一个函数。

定义

fn.call(obj, param1, param2, ...)
fn.apply(obj, [param1,param2,...])
fn.bind(obj, param1, param2, ...)

作用

call() 允许为不同的对象分配和调用属于一个对象的函数/方法。 call() 提供新的 this 值给当前调用的函数/方法。你可以使用 call 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)

如果第一个参数为为null的话,那么它们没有任何不同,如下代码片段片段:

function A(name) {
    this.name = name
    console.log('AAA')
}

function B(){
    A.call(null, 'A.call')
}

const b = new B()
console.log(b)
// AAA
// B {}

而当null -> this,之后:

function B() {
    A.call(this, '砂糖橘加盐')
}
const b = new B()
console.log(b) 
// AAA
// B { name: 'A.call' }

并且也成功打印出console的结果。还'意外'的让B的实例对象挂载了A构造函数的this对象。也就是之前说的,它能够改变当前函数的this

改变了之后又有什么作用呢?

我们可设想这么一个场景,当很多对象都存在相同的属性的时候,如果给每个构造函数都要添加一个的属性。如果可以抽离出来的话,我们是不是可以少写很多代码。如下代码片段:

function A(type) {
    this.type = type
}

function B() {
    A.call(this, '汽车')
}

function C() {
    A.call(this, '汽车')
}

function D() {
    A.call(this, '汽车')
}

let b = new B()
console.log(b);

let c = new C()
console.log(c);

let d = new D()
console.log(d);

// B { type: '汽车' }
// C { type: '汽车' }
// D { type: '汽车' }

call、apply、bind三者的区别

这三个方法共有的、比较明显的作用就是,都可以改变函数 func 的 this 指向

call、apply两者和bind的区别:返回的结果不一样,bind返回的是Function类型。如下:

const a = {
  name: '砂糖橘放盐',
  getName: function(msg) {
    return msg + this.name;
  } 
}
const b = {
  name: '橘子哥'
}
console.log(a.getName('你好,'));  // 你好, 砂糖橘放盐

const name = a.getName.bind(b, '你好,')
// 调用 a 的 getName():传入的参数 msg 为'你好,',因为 bind 修改了 this 指向,所以 this.name 指向 b 的 name 

console.log(name());  
// 你好, 橘子哥

call 和 apply 的区别在于:传参的写法不同

call的参数是列表,apply的则是数组形式

console.log(a.getName.call(b, '你好,'));  // 你好, 橘子哥
console.log(a.getName.apply(b, ['你好,']))  // 你好, 橘子哥

原型相关

小知识点

Object 是所有对象的爸爸,所有对象都可以通过 proto 找到它

Function 是所有函数的爸爸,所有函数都可以通过 proto 找到它

所有的函数都有prototype属性(原型)

所有的对象都有proto属性

什么是原型

原型分为隐式原型和显式原型,每个对象都有一个隐式原型(proto),它指向自己的构造函数的显式原型(prototype)

每一个构造函数都有一个 prototype 属性,这个属性会在生成实例的时候,成为实例对象的原型对象。javascript 的每个对象都继承另一个对象,后者称为“原型”(prototype)对象。

prototype是函数特有的属性(显式原型),指向其一个对象(默认指向一个空对象),__proto__是对象特有的属性(隐式原型),指向其继承的原型对象(或者说其构造函数的prototype)

原型.png

什么是原型链

每一个对象都有一个 proto 属性,proto 会指向自身的原型,由于原型本身也是对象,又有自己的原型。此时 proto 又会指向自身的原型,这样层层递进就形成了一条原型链

什么是继承

继承就是在子类构造函数中继承父类构造函数的私有属性和原型属性

变量声明提升与函数声明提升

变量声明提升和函数声明提升 就是将声明的变量或者函数,提升至所在作用域的顶部

函数的提升高于变量的提升

函数内部如果用 var 声明了相同名称的外部变量,函数将不再向上寻找。

匿名函数不会提升。

变量声明和函数声明

闭包

闭包是什么

能够访问其他函数内部变量的函数,被称为 闭包

上面这个定义比较难理解,简单来说,闭包就是函数内部定义的函数,被返回了出去并在外部调用。我们可以用代码来表述一下:

function foo() {
  var a = 2;

  function bar() {
    console.log( a );
  }

  return bar;
}

var baz = foo();

baz(); // 这就形成了一个闭包
复制代码

我们可以简单剖析一下上面代码的运行流程:

  1. 编译阶段,变量和函数被声明,作用域即被确定。
  2. 运行函数 foo(),此时会创建一个 foo 函数的执行上下文,执行上下文内部存储了 foo 中声明的所有变量函数信息。
  3. 函数 foo 运行完毕,将内部函数 bar 的引用赋值给外部的变量 baz ,此时 baz 指针指向的还是 bar ,因此哪怕它位于 foo 作用域之外,它还是能够获取到 foo 的内部变量。
  4. baz 在外部被执行,baz 的内部可执行代码 console.log 向作用域请求获取 a 变量,本地作用域没有找到,继续请求父级作用域,找到了 foo 中的 a 变量,返回给 console.log,打印出 2

闭包的执行看起来像是开发者使用的一个小小的 “作弊手段” ——绕过了作用域的监管机制,从外部也能获取到内部作用域的信息。闭包的这一特性极大地丰富了开发人员的编码方式,也提供了很多有效的运用场景。

闭包的本质

本质就是上级作用域内变量的生命周期,因为被下级作用域内引用,而没有被释放。就导致上级作用域内的变量,等到下级作用域执行完以后才正常得到释放。

闭包的应用场景

闭包的应用,大多数是在需要维护内部变量的场景下

闭包的最典型的应用是实现回调函数(callback)

优缺点

好处

可以读取函数内部的变量

将变量始终保持在内存中

可以封装对象的私有属性和私有方法

坏处

比较耗费内存、使用不当会造成内存溢出的问题

创建对象的几种方式

内置构造函数方式

// 在JavaScript中,对象具有动态性,可以随时给一个对象增删属性
var person = new Object()
person.name = 'tom'
person.eat = function () {
    // ...
}

缺点:很麻烦,每个属性都要手动添加

对象字面量方式

var person = { 
    name: 'tom',
    eat: function () {
        // ...
    }
}

缺点:无法有效的批量生成多个对象

简单改进:工厂函数方式

// 第一种写法
function createPerson (name) {
    return {
        name: name,
        eat: function () {
            // ...
        }
    }
}
let p = createPerson('tom')

// 第二种写法
function setObj (name) {
    let obj = new Object()
    obj.name = name
    obj.eat = function () {
        // ...
    }
}
let obj = setObj('tom')

缺点:无法识别对象,创建出来的对象都是Object类

继续改进:构造函数方式

function Person (name) {
    this.name = name
    this.eat = function () {
        // ...
    }
}
var p = new Person('tom')