JavaScript核心知识总结(中)一https://developer.aliyun.com/article/1495053
原型链
JavaScript
的所有对象都有一个__proto__
属性,这个属性所对应的就是该对象的原型JavaScript
的函数对象除了原型__proto__
之外,还预置了prototype
属性(Function.prototype.bind
没有)- 当函数对象作为构造函数创建实例时,其
prototype
属性值将被作为实例对象的原型__proto__
constructor
constructor 返回创建实例对象时构造函数的引用
function Parent(age) { this.age = age; } var p = new Parent(50); p.constructor === Parent; // true p.constructor === Object; // false
prototype
这是一个显式原型属性,只有函数才拥有该属性。基本所有的函数都有这个属性,除了Function.prototype.bind()
let fun = Function.prototype.bind()
如果你以上述方法创建一个函数,那么可以发现这个函数是不具有 prototype
属性的。
prototype是如何产生的
当我们声明一个函数时,这个属性就被自动创建了
function Foo(){}
并且这个属性的值是一个对象(也就是原型),只有一个属性constructor
constructor
对应着构造函数,也就是Foo
proto
这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]]
,但是 [[prototype]]
是内部属性,我们并不能访问到,所以使用__proto__
来访问。
在 new
的过程中,新对象被添加了 __proto__
并且链接到构造函数的原型上。
Function.proto === Function.prototype
对于对象来说,obj.__proto__.constructor
是该对象的构造函数,但是上图标明Function.__proto__=== Function.prototype
。从图中可以发现,所有对象都可以通过原型链最终找到Object.prototype
,虽然Object.prototype
也是一个对象,但是这个对象却不是Object
创造的,而是引擎创建的。
引擎创建了 Object.prototype
,然后创建了 Function.prototype
,并且通过 __proto__
将两者联系了起来。这里也很好的解释了上面的一个问题,为什么 let fun = Function.prototype.bind()
没有 prototype
属性。因为 Function.prototype
是引擎创建出来的对象,引擎认为不需要给这个对象添加 prototype
属性。
所以我们又可以得出一个结论,不是所有函数都是 new Function()
产生的。
有了 Function.prototype
以后才有了 function Function()
,然后其他的构造函数都是 function Function()
生成的。
现在可以来解释 Function.__proto__ === Function.prototype
这个问题了。因为先有的 Function.prototype
以后才有的 function Function()
,所以也就不存在鸡生蛋蛋生鸡的悖论问题了。对于为什么 Function.__proto__
会等于 Function.prototype
,个人的理解是:其他所有的构造函数都可以通过原型链找到 Function.prototype
,并且 function Function()
本质也是一个函数,为了不产生混乱就将 function Function()
的 __proto__
联系到了 Function.prototype
上。
原型小结
Object
是所有对象的爸爸,所有对象都可以通过__proto__
找到它Function
是所有函数的爸爸,所有函数都可以通过__proto__
找到它Function.prototype
和Object.prototype
是两个特殊的对象,由引擎创建- 除了以上两个对象,其他对象都是通过构造器
new
创建出来的 - 函数的
prototype
是一个对象,也就是原型 - 对象的
__proto__
指向原型,__proto__
将对象和原型连接起来组成了原型链
instanceof 判断对象的原理是什么?
判断实例对象的__proto__
属性与构造函数的prototype
是不是用一个引用。如果不是,他会沿着对象的__proto__
向上查找的,直到为null
。
const Person = function(){} const p1 = new Person() p1 instanceof Person//true var str = 'hello world' str instanceof String // true var str1 = new String('hello world') str1 instanceof String // true
自己实现一个 instanceof
function instance_of(L, R) { var O = R.prototype; L = L.__proto__; while (true) { if (L === null) return false;//最终没找到,返回false if (O === L) return true;//相等则返回true L = L.__proto__;//继续沿着__proto__向上找 } }
继承
类的区别
//ES5 function Animal(){ this.name = 'Animal' } //ES6 class Animal2{ constructor () { this.name = 'Animal'; } }
原型链继承
function Cat(){ } Cat.prototype=new Animal() var cat = new Cat()
原理:把子类的prototype
(原型对象)直接设置为父类的实例
缺点:因为子类只进行一次原型更改,所以子类的所有实例保存的是同一个父类的值。 当子类对象上进行值修改时,如果是修改的原始类型的值,那么会在实例上新建这样一个值; 但如果是引用类型的话,他就会去修改子类上唯一一个父类实例里面的这个引用类型,这会影响所有子类实例
构造继承
function Cat(){ Animal.call(this) }
原理: 将子类的this
使用父类的构造函数跑一遍
缺点: Parent原型链上的属性和方法并不会被子类继承
实例继承
function Cat(name){ var instance = new Animal() instance.name = name || 'cat' return instance }
组合继承
function Cat(){ Animal.call(this) } Cat.prototype = new Animal() Cat.prototype.constructor = Cat
特点:
- 弥补了方式2的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
- 既是子类的实例,也是父类的实例
- 不存在引用属性共享问题
- 可传参
- 函数可复用 缺点:
- 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
拷贝继承
function Cat(name){ var animal = new Animal(); for(var p in animal){ Cat.prototype[p] = animal[p]; } Cat.prototype.name = name || 'Tom'; } var cat = new Cat()
特点:
- 支持多继承 缺点:
- 效率较低,内存占用高(因为要拷贝父类的属性) 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
寄生组合继承
function Cat(){ Animal.call(this) } (function(){ var Super = function(){} Super.prototype = Animal.prototype Cat.prototype = new Super() })() Cat.prototype.constructor = Cat var cat = new Cat()
ES5/ES6 的继承除了写法以外还有什么区别?
- class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 let、const 声明变量。
- class 声明内部会启用严格模式。
- class 的所有方法(包括静态方法和实例方法)都是不可枚举的。
- class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。
- 必须使用 new 调用 class。
- class 内部无法重写类名。
事件循环
为什么JavaScript是单线程?
JavaScript
语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript
不能有多个线程呢?这样能提高效率啊。
JavaScript
的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript
的主要用途是与用户互动,以及操作DOM
。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript
同时有两个线程,一个线程在某个DOM
节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript
就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
为了利用多核CPU
的计算能力,HTML5
提出Web Worker标准,允许JavaScript
脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM
。所以,这个新标准并没有改变JavaScript
单线程的本质。
Event Loop
参考地址:Event Loop 这个循环你晓得么?(附 GIF 详解)-饿了么前端
任务队列的本质
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
- 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步。
异步任务
setTimeOut
、setInterval
DOM
事件Promise
可分为宏任务和微任务,分类如下:
- 宏任务:setTimeout,setInterval,setImmediate,I/O(磁盘读写或网络通信),UI交互事件
- 微任务:process.nextTick,Promise.then
前面我们介绍,事件循环会将其中的异步任务按照执行顺序排列到事件队列中。然而,根据异步事件的不同分类,这个事件实际上会被排列到对应的宏任务队列或者微任务队列当中去。
当执行栈中的任务清空,主线程会先检查微任务队列中是否有任务,如果有,就将微任务队列中的任务依次执行,直到微任务队列为空,之后再检查宏任务队列中是否有任务,如果有,则每次取出第一个宏任务加入到执行栈中,之后再清空执行栈,检查微任务,以此循环... ...
JavaScript 实现异步编程的方法?
- 回调函数
- 事件监听
- 发布/订阅
- Promise 对象
- Async 函数[ES7]
关于 setTimeOut、setImmediate、process.nextTick()的比较
setTimeout()
将事件插入到了事件队列,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。 当主线程时间执行过长,无法保证回调会在事件指定的时间执行。 浏览器端每次setTimeout
会有4ms
的延迟,当连续执行多个setTimeout
,有可能会阻塞进程,造成性能问题。
setImmediate()
事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行。和setTimeout(fn,0)
的效果差不多。 服务端 node
提供的方法。浏览器端最新的 api
也有类似实现:window.setImmediate
,但支持的浏览器很少。
process.nextTick()
插入到事件队列尾部,但在下次事件队列之前会执行。也就是说,它指定的任务总是发生在所有异步任务之前,当前主线程的末尾。 大致流程:当前”执行栈”的尾部–>下一次Event Loop
(主线程读取”任务队列”)之前–>触发 process
指定的回调函数。 服务器端 node
提供的办法。用此方法可以用于处于异步延迟的问题。 可以理解为:此次不行,预约下次优先执行。
Promise
Promise
本身是同步的立即执行函数, 当在 executor
中执行 resolve
或者 reject
的时候, 此时是异步操作, 会先执行 then/catch
等,当主栈完成后,才会去调用 resolve/reject
中存放的方法执行,打印 p
的时候,是打印的返回结果,一个 Promise
实例。
async await
Async/Await
就是一个自执行的 generate
函数。利用generate
函数的特性把异步的代码写成“同步”的形式。
async
函数返回一个 Promise
对象,当函数执行的时候,一旦遇到 await
就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async
函数体。
正则
特殊字符
- ^ 匹配输入的开始
- $ 匹配输入的结束
- * 匹配0次或多次{0, }
- + 匹配1次或多次{1, }
- ?
- 0次或者1次{0,1}
- 用于先行断言
- 如果紧跟在任何量词 *、 +、? 或 {} 的后面,将会使量词变为非贪婪
- 对 "123abc" 用 /\d+/ 将会返回 "123",
- 用 /\d+?/,那么就只会匹配到 "1"。
- . 匹配除换行符之外的任何单个字符
- (x) 匹配 'x' 并且记住匹配项
- (?:x) 匹配 'x' 但是不记住匹配项
- x(?=y) 配'x'仅仅当'x'后面跟着'y'.这种叫做正向肯定查找。
- x(?!y) 匹配'x'仅仅当'x'后面不跟着'y',这个叫做正向否定查找。
- x|y 匹配‘x’或者‘y’。
- {n} 重复n次
- {n, m} 匹配至少n次,最多m次
- [xyz] 代表 x 或 y 或 z
- [^xyz] 不是 x 或 y 或 z
- \d 数字
- \D 非数字
- \s 空白字符,包括空格、制表符、换页符和换行符。
- \S 非空白字符
- \w 单词字符(字母、数字或者下划线) [A-Za-z0-9_]
- \W 非单字字符。[^A-Za-z0-9_]
- \3 表示第三个分组
- \b 词的边界
- /\bm/匹配“moon”中得‘m’;
- \B 非单词边界
正则表达式的方法
- exec 一个在字符串中执行查找匹配的RegExp方法,它返回一个数组(未匹配到则返回null)
- test 一个在字符串中执行查找匹配的RegExp方法,返回true或者false
- match 一个在字符串中执行查找匹配的String方法,它返回一个数组或者在未匹配到时返回null。
- search 一个在字符串中测试匹配的String方法,它返回匹配到的位置索引,或者在失败时返回-1。
- replace 一个在字符串中执行查找匹配的String方法,并且使用替换字符串替换掉匹配到的子字符串。
- split 一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的String方法。
练习
匹配结尾的数字
/\d+$/g
统一空格个数
字符串内如有空格,但是空格的数量可能不一致,通过正则将空格的个数统一变为一个。
let reg = /\s+/g str.replace(reg, " ");
判断字符串是不是由数字组成
str.test(/^\d+$/);
电话号码正则
- 区号必填为3-4位的数字
- 区号之后用“-”与电话号码连接电话号码为7-8位的数字
- 分机号码为3-4位的数字,非必填,但若填写则以“-”与电话号码相连接
/^\d{3,4}-\d{7,8}(-\d{3,4})?$/
手机号码正则表达式
正则验证手机号,忽略前面的0,支持130-139,150-159。忽略前面0之后判断它是11位的。
/^0*1(3|5)\d{9}$/
使用正则表达式实现删除字符串中的空格
function trim(str) { let reg = /^\s+|\s+$/g return str.replace(reg, ''); }
限制文本框只能输入数字和两位小数点等等
/^\d*\.\d{0,2}$/
只能输入小写的英文字母和小数点,和冒号,正反斜杠(:./)
/^[a-z\.:\/\\]*$/
替换小数点前内容为指定内容
例如:infomarket.php?id=197 替换为 test.php?id=197
var reg = /^[^\.]+/; var target = '---------'; str = str.replace(reg, target)
只匹配中文的正则表达式
/[\u4E00-\u9FA5\uf900-\ufa2d]/ig
返回字符串的中文字符个数
先去掉非中文字符,再返回length属性。
function cLength(str){ var reg = /[^\u4E00-\u9FA5\uf900-\ufa2d]/g; //匹配非中文的正则表达式 var temp = str.replace(reg,''); return temp.length; }
正则表达式取得匹配IP地址前三段
只要匹配掉最后一段并且替换为空字符串就行了
function getPreThrstr(str) { let reg = /\.\d{1,3}$/; return str.replace(reg,''); }
匹配<ul>与</ul>之间的内容
/<ul>[\s\S]+?</ul>/i
用正则表达式获得文件名
c:\images\tupian\006.jpg 可能是直接在盘符根目录下,也可能在好几层目录下,要求替换到只剩文件名。 首先匹配非左右斜线字符0或多个,然后是左右斜线一个或者多个。
function getFileName(str){ var reg = /[^\\\/]*[\\\/]+/g; // xxx\ 或是 xxx/ str = str.replace(reg,''); return str; }
绝对路径变相对路径
"http://23.123.22.12/image/somepic.gif"转换为:"/image/somepic.gif"
var reg = /http:\/\/[^\/]+/; str = str.replace(reg,"");
用户名正则
用于用户名注册,,用户名只 能用 中文、英文、数字、下划线、4-16个字符。
/^[\u4E00-\u9FA5\uf900-\ufa2d\w]{4,16}$/
匹配英文地址
规则如下: 包含 "点", "字母","空格","逗号","数字",但开头和结尾不能是除字母外任何字符。
/^[a-zA-Z][\.a-zA-Z,0-9]*[a-zA-Z]$/
正则匹配价格
开头数字若干位,可能有一个小数点,小数点后面可以有两位数字。
/^\d+(\.\d{2})?$/
身份证号码的匹配
身份证号码可以是15位或者是18位,其中最后一位可以是X。其它全是数字
/^(\d{14}|\d{17})(X|x)$/
单词首字母大写
每单词首字大写,其他小写。如blue idea转换为Blue Idea,BLUE IDEA也转换为Blue Idea
function firstCharUpper(str) { str = str.toLowerCase(); let reg = /\b(\w)/g; return str.replace(reg, m => m.toUpperCase()); }
正则验证日期格式
yyyy-mm-dd格式 4位数字,横线,1或者2位数字,再横线,最后又是1或者2位数字。
js
复制代码
/^\d{4}-\d{1,2}-\d{1,2}$/
去掉文件的后缀名
www.abc.com/dc/fda.asp 变为 www.abc.com/dc/fda
function removeExp(str) { return str.replace(/\.\w$/,'') }
验证邮箱的正则表达式
开始必须是一个或者多个单词字符或者是-,加上@,然后又是一个或者多个单词字符或者是-。然后是点“.”和单词字符和-的组合,可以有一个或者 多个组合。
/^[\w-]+@\w+\.\w+$/
正则判断标签是否闭合
例如:<img xxx=”xxx” 就是没有闭合的标签;
p的内容,同样也是没闭合的标签。
标签可能有两种方式闭合, 或者是
xxx
。
/<([a-z]+)(\s*\w*?\s*=\s*".+?")*(\s*?>[\s\S]*?(<\/\1>)+|\s*\/>)/i
正则判断是否为数字与字母的混合
不能小于12位,且必须为字母和数字的混
/^(([a-z]+[0-9]+)|([0-9]+[a-z]+))[a-z0-9]*$/i
将阿拉伯数字替换为中文大写形式
function replaceReg(reg,str){ let arr=["零","壹","贰","叁","肆","伍","陆","柒","捌","玖"]; let reg = /\d/g; return str.replace(reg,function(m){return arr[m];}) }
去掉标签的所有属性
<td style="width: 23px; height: 26px align="left">***</td> 变成没有任何属性的 <td>***</td>
思路:非捕获匹配属性,捕获匹配标签,使用捕获结果替换掉字符串。正则如下
/(<td)\s(?:\s*\w*?\s*=\s*".+?")*?\s*?(>)/
垃圾回收
JavaScript垃圾回收
标记清除(mark and sweep)
- 这是
JavaScript
最常见的垃圾回收方式,当变量进入执行环境的时候,比如函数中声明一个变量,垃圾回收器将其标记为“进入环境”,当变量离开环境时(函数执行结束)将其标记为“离开环境” - 垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中的变量所引用的变量(闭包),在这些完成后仍存在标记的就是要删除的变量了。
引用计数(reference counting)
- 在低版本
IE
中经常会出现内存泄漏,很多时候就是因为采取引用计数的方式进行垃圾回收。引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个变量并将一个引用类型赋值给改变量的时候这个值的引用次数加1,如果该变量的值变成了另外一个,则这个值的引用次数减1,当这个值的引用次数变为0的时候,说明没有变量在引用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为0的值占用的空间
参考链接 内存管理-MDN
V8下的垃圾回收机制
V8
实现了准确式GC,GC
算法采用了分代式垃圾回收机制。因此,V8
将内存(堆)分为新生代和老生代
- 新生代算法
- 新生代中对象一般存活时间较短,使用
Scavenge GC
算法。
- 在新生代空间中,内存空间分成两块,分别为
From
空间和To
空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入From
空间中,当From
空间被沾满时,新生代GC
就会启动了。算法会检测From
空间中存活的对象并复制到To
空间中,如果有失活的对象就会销毁。当复制完成将From
空间和To
空间互换,这样GC
就结束了
- 老生代算法
- 老生代中的对象一般存活时间比较长且数量也多,使用了两个算法,分别是标记清除算法(Mark-Sweep)和标记压缩算法(Mark-Compact)。对象出现在老生代空间的情况如下
- 新生代中的对象是否已经经历过一次
Scavenge
算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。 To
空间的对象占比大小超过25%
。在这种情况下为了不影响到内存分配,会将对象从新生代空间移动到老生代空间。
- 标记清除(Mark-Sweep)
- Mark-Sweep在标记阶段遍历堆中的所有对象,并标记活着的对象,在随后的清除阶段中,只清楚没有标记的对象。Mark-Sweep最大的问题是在进行一次标记清除回收后,内存空间出现不连续的状态,为了解决这个内存碎片的问题,Mark-Compact被提出。
- Mark-Compact在整理的过程中,将活着的对象往一端移动,移动完成后,直接清除掉边界外的内存。
内存
申请的内存没有及时回收掉,则导致内存
为什么会发生内存泄漏?
虽然前端有垃圾回收机制,但当某块无用的内存,却无法被垃圾回收机制认为是垃圾时,也就发生内存泄漏了
而垃圾回收机制通常是使用标志清除策略,简单说,也就是引用从根节点开始是否可达来判定是否是垃圾
上面是发生内存泄漏的根本原因,直接原因则是,当不同生命周期的两个东西相互通信时,一方生命到期该回收了,却被另一方还持有时,也就发生内存泄漏了
哪些情况会引起内存泄漏
- 意外的全局变量
- 遗忘的定时器
- 使用不当的闭包
- 遗漏的DOM元素
- 网络回调
如何监控内存泄漏
内存泄漏是可以分成两类的,一种是比较严重的,泄漏的就一直回收不回来了,另一种严重程度稍微轻点,就是没有及时清理导致的内存泄漏,一段时间后还是可以被清理掉
不管哪一种,利用开发者工具抓到的内存图,应该都会看到一段时间内,内存占用不断的直线式下降,这是因为不断发生 GC
,也就是垃圾回收导致的
针对第一种比较严重的,会发现,内存图里即使不断发生 GC
后,所使用的内存总量仍旧在不断增长
另外,内存不足会造成不断 GC
,而 GC
时是会阻塞主线程的,所以会影响到页面性能,造成卡顿,所以内存泄漏问题还是需要关注的
举例场景
在某个函数内申请一块内存,然后该函数在短时间内不断被调用
// 点击按钮,就执行一次函数,申请一块内存 startBtn.addEventListener("click", function() { var a = newArray(100000).fill(1); var b = newArray(20000).fill(1); });
一个页面能够使用的内存是有限的,当内存不足时,就会触发垃圾回收机制去回收没用的内存
而在函数内部使用的变量都是局部变量,函数执行完毕,这块内存就没用可以被回收了
所以当我们短时间内不断调用该函数时,可以发现,函数执行时,发现内存不足,垃圾回收机制工作,回收上一个函数申请的内存,因为上个函数已经执行结束了,内存无用可被回收了
如何分析内存,找出有问题的代码
借助开发者工具的memory
功能
最后
行文至此,感谢阅读,一键三连是对我最大的支持。