【前端重温系列】this的不可描述与变化原理(献给2020-1024的礼物)
时间已经是2020年了,马上也就2021年了,现如今的发展,ES6的实现几乎现代化浏览器都实现了。红宝书也出第四版了,删除了过旧的知识,引入了ES6,涵盖至ECMAScript 2019新标准。新的基础工具书的推出,自己也该好好重温一遍基础,重新梳理自己脑海的知识点,过时的删除,腾出空间给新知识。当然,内容主要还是从核心知识开始,扩展性涵盖其他知识点。
今天是本次前端重温系列的第二篇,有关于this的指向原则和call/apply/bind等原理。欢迎各位看官收看。
js的内存管理和堆栈
谈到this,就不得不说一说javascript的内存管理机制。
有许多语言会暴露内存管理的api给开发者,也有的语言会自己默默的独自完成内存的管理操作。
内存生命周期
不管什么程序语言,内存生命周期基本是一致的:
- 分配你所需要的内存
- 值的初始化:js在定义变量时就完成了内存分配
- 通过函数调用分配内存
- 使用分配到的内存(读、写)
- 不需要时将其释放\归还
栈内存与堆内存
js有两大数据类型:基本类型和引用类型
基本类型往往在栈内存里存储,而引用类型往往在堆内存里存储。
堆和栈分别是不同的数据结构。栈是线性表的一种,而堆则是树形结构。
垃圾回收
垃圾回收算法主要依赖于引用的概念。在内存管理的环境中,一个对象如果有访问另一个对象的权限(隐式或者显式),叫做一个对象引用另一个对象。例如,一个Javascript对象具有对它原型的引用(隐式引用)和对它属性的引用(显式引用)。“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)。
引用计数垃圾收集
- 这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。
标记-清除算法
- 这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象)。垃圾回收器将定期从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象。从根开始,垃圾回收器将找到所有可以获得的对象和收集所有不能获得的对象。从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。所有对JavaScript垃圾回收算法的改进都是基于标记-清除算法的改进,并没有改进标记-清除算法本身和它对“对象是否不再需要”的简化定义。
内存泄漏
内存泄漏的概念
该释放的变量(内存垃圾)没有被释放,仍然霸占着原有的内存不松手,导致内存占用不断攀高,带来性能恶化、系统崩溃等一系列问题,这种现象就叫内存泄漏。
1 | var theThing = null; |
这段代码有什么问题吗?
在 V8 中,一旦不同的作用域位于同一个父级作用域下,那么它们会共享这个父级作用域。unused 是一个不会被使用的闭包,但和它共享同一个父级作用域的 someMethod,则是一个 “可抵达”(也就意味着可以被使用)的闭包。unused 引用了 originalThing,这导致和它共享作用域的 someMethod 也间接地引用了 originalThing。结果就是 someMethod “被迫” 产生了对 originalThing 的持续引用,originalThing 虽然没有任何意义和作用,却永远不会被回收。不仅如此,originalThing 每次 setInterval 都会改变一次指向(指向最近一次的 theThing 赋值结果),这导致无法被回收的无用 originalThing 越堆积越多,最终导致严重的内存泄漏。
可能导致内存泄漏的写法
- 无意义的全局变量
1 | function a() { |
- 未清除的setInterval和链式调用的setTimeout
1 | setInterval(function() { |
1 | setTimeout(function() { |
- 清除不当的变量
1 | var myDiv = document.getElementById('myDiv') |
this的指向原则
this指向:指向执行时所在的上下文,即被调用函数所在的对象
this的指向由函数执行时确定,而不是定义时决定的。这点和闭包恰恰相反。当调用方法没有明确对象时,则是指向window
如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象
1 | var o = { |
this永远指向的是最后调用它的对象,也就是看它执行的时候是谁调用的
如果 new 关键词出现在被调用函数的前面,那么JavaScript引擎会创建一个新的对象,被调用函数中的this指向的就是这个新创建的函数。
如果通过apply、call或者bind的方式触发函数,那么函数中的this指向传入函数的第一个参数
如果一个函数是某个对象的方法,并且对象使用句点符号触发函数,那么this指向的就是该函数作为那个对象的属性的对象,也就是,this指向句点左边的对象。
this特殊情形
this必然指向window的情况
- 立即执行函数(IIFE)
- setTimeout 中传入的函数
- setInterval 中传入的函数
严格模式的情形
- 严格模式下,this 将保持它被指定的那个对象的值,所以,如果没有指定对象,this 就是 undefined
箭头函数
- 箭头函数中的 this,和你如何调用它无关,由你书写它的位置决定
如果返回值是一个Object,那么this指向的就是那个返回的对象,否则指向函数的实例
1 | // null |
改变this
改变this的方法途径
- 书写定义时改变,比如箭头函数
- 调用时改变,显式地调用一些方法,比如call/apply/bind
箭头函数
箭头函数是在定义的时候就决定了指向
1 | var a = 1 |
构造函数:构造函数里面的 this 会绑定到我们 new 出来的这个对象上
显式调用
call/apply/bind的特点
- call
- 改变后直接调用
- fn.call(ctx, arg1, arg2)
- apply
- 改变后直接调用
- fn.apply(ctx, [arg1, arg2])
- bind
- 改变后不进行调用操作
- fn.bind(ctx, arg1, arg2)
实现call/apply/bind方法
可以看此文章,写的很详细:
后记
这一篇文章由于内容涵盖了的知识比较的偏底层和js语法的特性,在准备花费的时间较长,由于这个阶段自己正好在寻找工作,断断续续的在填坑,最后的内容是在去入职的火车上完成的哈哈哈。算是比较有意义的一个纪念!特写了个后记记录一下。
同时今天又是1024!我们的狂欢🎉!献给2020-1024的礼物!
【前端重温系列】this的不可描述与变化原理(献给2020-1024的礼物)