JavaScript核心知识总结(中)二

简介: JavaScript核心知识总结(中)二

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

14.png

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

15.png


对于对象来说,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.prototypeObject.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)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
  • 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  • 主线程不断重复上面的第三步。

异步任务

  • setTimeOutsetInterval
  • 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实现了准确式GCGC算法采用了分代式垃圾回收机制。因此,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功能

最后

行文至此,感谢阅读,一键三连是对我最大的支持。

相关文章
|
6月前
|
缓存 JavaScript 前端开发
JavaScript核心知识总结(下)一
JavaScript核心知识总结(下)一
|
6月前
|
存储 自然语言处理 JavaScript
JavaScript核心知识总结(中)一
JavaScript核心知识总结(中)一
|
6月前
|
Web App开发 存储 JavaScript
JavaScript核心知识总结(上)
JavaScript核心知识总结(上)
|
6月前
|
JavaScript 前端开发 应用服务中间件
JavaScript核心知识总结(下)二
JavaScript核心知识总结(下)二
|
JavaScript 前端开发
3 JavaScript基础使用
读前思考! 在使用之前要先明白2个问题: 在哪里写(用)JavaScript代码? 如何写JavaScript代码?
92 0
|
存储 JavaScript 前端开发
JavaScript基础第02
JavaScript基础第02
145 0
|
JavaScript 前端开发
JavaScript基础之for
JavaScript基础之for
97 0
|
前端开发 JavaScript API
155个JavaScript基础问题(126-135)
155个JavaScript基础问题(126-135)
231 0
155个JavaScript基础问题(126-135)
|
JavaScript 前端开发
155个JavaScript基础问题(1-5)
155个JavaScript基础问题(1-5)
172 0
155个JavaScript基础问题(1-5)
|
JavaScript 前端开发 API
155个JavaScript基础问题(86-95)
155个JavaScript基础问题(86-95)
140 0
155个JavaScript基础问题(86-95)
下一篇
无影云桌面