js变量、作用域、内存总结

变量、作用域、内存

一、变量

1.基本类型和引用类型

基本类型:简单数据段,存放在栈中,按值访问
引用类型:多个值构成的对象,按引用访问
基本:undefined,null,boolean,number,string,symbol,bigint

2.类型检测

1. typeof
  1. typeof返回一个表示数据类型的字符串,返回结果包括:string、number、boolean、symbol、undefined、function、object。
    -typeof无法检测出null和array的类型,检测出来这两个都是object。
typeof Symbol(); // symbol 有效 
typeof ''; // string 有效 
typeof 1; // number 有效 
typeof true; //boolean 有效 
typeof undefined; //undefined 有效 
typeof new Function(); // function 有效
typeof null; //object 无效 
typeof [] ; //object 无效 
typeof new Date(); //object 无效 
typeof new RegExp(); //object 无效
2. instanceof

instanceof:能够检测具体是什么类型的对象
-语法:a instanceof Object
b instanceof Array
-返回值:boolean

[] instanceof Array // true 
{} instanceof Object // true 
new Date() instanceof Date // true
new RegExp() instanceof RegExp // true 
Array.isArray([]); // true

instanceof的三大弊端

1.对于基本数据类型来说,字面量方式创建出来的结果和实例方式创建出来的是有区别的。从严格意义上来讲,只有实例创建出来的结果才是标准的对象数据类型值,也是标准的 Number 这个类的一个实例;对于字面量方式创建出来的结果是基本的数据类型值,不是严谨的实例,但是由于 JS 的松散特点,导致了其可以使用 Number.prototype 上提供的方法。
console.log(1 instance of Number)//false
console.log(new Number(1) instanceof Number)//true

2.只要在当前实例的原型链上,我们用其检测出来的结果都是true。但在类的原型继承中,我们最后检测出来的结果未必准确。

3.对于特殊的数据类型nullundefined。他们所属的类是Null和Undefined。但是浏览器把这两个类保护起来了,不允许我们在外部访问

img

3. ===

只能用于判断基本数据类型数据。

let aaaa = null;
let bbbb = null;
console.log(aaaa===bbbb)//true

let aaaa = undefined;
let bbbb = undefined;
console.log(aaaa===bbbb)//true
4. constructor

constructor可以检测当前对象的直属类,而不能检测原型链上的所有。它还可以处理基本数据类型的检测

 let arr = new Array();
 console.log(arr.constructor===Array)//true
 console.log(arr.constructor===Object)//false
 console.log((1).constructor === Number)//true
 console.log(reg.constructor=== RegExp);//true

弊端

nullundefined是无效的对象,因此不会有constructor存在的。

② 函数的constructor不稳定,主要是类的原型重写时,在重写的过程中可能会把之前的constructor覆盖掉,这样检测出来的结果是不准确的。

5.Object.prototype.toString.call()
  • Object原型链上的toString方法不是用来转换成字符串的。它的作用是返回当前方法执行的主体(方法中的this)所属类的详细信息,即[object Object],其中
    • 第一个object代表当前实例是对象数据类型的(这个是固定不变的),
    • 第二个object代表的当前this所属类的名称。
  • 所有类都是继承于Object类,故在创建所有类的实例时,理应来说可以调用到Object原型对象的toString方法。
  • 而除了Object类的实例以外,其余的类调用toString方法都是将其本身输出,这是因为所有类在继承Object类时重写了toString方法,故在调用时,调用的都是重写过后的toString方法,而非Object原型对象上的toString方法。当把所有类上的重写后的toString方法删除后,在通过该类实例调用toString方法时,调用的便是Object原型对象上的方法了(见例2)
  • 一般只有实例可以调用toString方法,但我们在实践中发现,1/true/"111"也可以调用toString方法,这是因为在调用该方法时,会将原始值包装成对象后再进行调用
Object.prototype.toString.call('') ; // [object String] 
Object.prototype.toString.call(1) ; // [object Number] 
Object.prototype.toString.call(true) ; // [object Boolean] Object.prototype.toString.call(undefined) ; // [object Undefined] Object.prototype.toString.call(null) ; // [object Null] 
Object.prototype.toString.call(new Function()) ; // [object Function] 
Object.prototype.toString.call(new Date()) ; // [object Date] 
Object.prototype.toString.call([]) ; // [object Array] 
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call( newError()) ; // [object Error]
Object.prototype.toString.call( document) ; // [object HTMLDocument]
Object.prototype.toString.call( window) ; //[object global] window是全局对象global的引用
//例2
// 定义一个数组
var arr = [1, 2, 3]
// 数组原型上是否具有 toString() 方法
console.log(Array.prototype.hasOwnProperty('toString')) //true
// 数组直接使用自身的 toString() 方法
console.log(arr.toString()) // '1,2,3'
// delete操作符删除数组原型上的 toString()
delete Array.prototype.toString
// 删除后,数组原型上是否还具有 toString() 方法
console.log(Array.prototype.hasOwnProperty('toString')) //false
// 删除后的数组再次使用 toString() 时,会向上层访问这个方法,即 Object 的 toString()
console.log(arr.toString()) // '[object Array]'

二、作用域

1.执行环境

执行环境定义了变量或函数有权访问其他数据,决定了他们各自的行为。每个执行环境都有与之关联的变量对象,环境中定义的变量和函数都保存在这个变量中。解析器会使用

  • 全局执行环境:windows对象

  • 函数执行环境:执行流进入一个函数使,函数的环境就会推入一个环境栈中,执行完退回之前的环境

2.作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链,保证对执行环境有权访问的所有变量和函数有序访问。

作用域链开头:是当前执行代码所在的环境的变量对象

作用域的结尾:是全局作用域环境的变量对象

当前环境的变量对象->包含环境的变量对象->下一个包含环境的变量对象->…全局的

标识符的解析就是沿着作用域链一步一步搜索的,内部环境可以通过作用域链访问外部环境,但是外部环境不能访问内部环境

延长作用域链:

有些语句可以在作用域链前端添加一个变量对象

1.with语句,添加指定变量对象,with语句中最接近的环境是函数的环境
2.try catch中的catch块,添加新的变量对象

三、内存

1.垃圾收集

原理:周期性的执行垃圾回收代码

标记清除:

进入环境标记内存中所有的变量,然后删除环境中的变量以及环境中变量引用的变量的标记。

周期性删除有标记的变量,回收空间

引用计数:

记录被引用次数,为0删除回收

1.声明一个变量,并且将一个引用类型赋值给变量 +1

2.同一个值又被赋值给另一变量 +1

3.如果包含这个值引用的变量又得到了另一个值,那么这个值-1

弊端:循环引用

a有一个指针指向b,b又有一个指针指向a

function problem(){
    let A = new Object(); 1
    let B = new Object(); 1
    A.someObject = B; 2
    B.someObject = A; 2
}

引用数一直是2不可能清除

2.性能问题

按内存分配量:几个指标,达到就会启动回收,问题:一个脚本很可能就包含这么多变量

指标动态修正,垃圾回收的内存低于15%,指标加倍,大于85%,回到默认值

3.管理内存

解除引用 = null

4.隐藏类和删除操作

V8 在将解释后的 JavaScript代码编译为实际的机器码时会利用“隐藏类,运行期间,V8 会将创建的对象与隐藏类关联起来,以跟踪它们的属性特征。能够共享相同隐藏类的对象性能会更好,V8 会针对这种情况进行优化,但不一定总能够做到

function Article() { 
 this.title = 'Inauguration Ceremony Features Kazoo Band'; 
} 
let a1 = new Article(); 
let a2 = new Article();

V8 会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原

型。假设之后又添加了下面这行代码:

a2.author = 'Jake';

此时两个 Article 实例就会对应两个不同的隐藏类。根据这种操作的频率和隐藏类的大小,这有

可能对性能产生明显影响。

当然,解决方案就是避免 JavaScript 的“先创建再补充”(ready-fire-aim)式的动态属性赋值,并在构造函数中一次性声明所有属性,如下所示:

function Article(opt_author) {
	this.title = 'Inauguration Ceremony Features Kazoo Band'; 
	this.author = opt_author; 
} 
let a1 = new Article(); 
let a2 = new Article('Jake');

这样,两个实例基本上就一样了(不考虑 hasOwnProperty 的返回值),因此可以共享一个隐藏类,从而带来潜在的性能提升。不过要记住,使用 delete 关键字会导致生成相同的隐藏类片段。看一下这个例子:

function Article() { 
    this.title = 'Inauguration Ceremony Features Kazoo Band'; 
    this.author = 'Jake'; 
} 
let a1 = new Article(); 
let a2 = new Article(); 
delete a1.author;

在代码结束后,即使两个实例使用了同一个构造函数,它们也不再共享一个隐藏类。动态删除属性与动态添加属性导致的后果一样。最佳实践是把不想要的属性设置为 null。这样可以保持隐藏类不变和继续共享,同时也能达到删除引用值供垃圾回收程序回收的效果。比如:

function Article() { 
    this.title = 'Inauguration Ceremony Features Kazoo Band'; 
    this.author = 'Jake'; 
} 
let a1 = new Article(); 
let a2 = new Article(); 
a1.author = null;

5.内存泄漏

  • 意外声明全局变量是最常见但也最容易修复的内存泄漏问题。下面的代码没有使用任何关键字声明变量:
function setName() { 
	name = 'Jake'; 
}

此时,解释器会把变量 name 当作 window 的属性来创建(相当于 window.name = ‘Jake’)。可想而知,在 window 对象上创建的属性,只要 window 本身不被清理就不会消失。这个问题很容易解决,只要在变量声明前头加上 var、let 或 const 关键字即可,这样变量就会在函数执行完毕后离开作用域。

  • 定时器也可能会悄悄地导致内存泄漏。下面的代码中,定时器的回调通过闭包引用了外部变量
let name = 'Jake'; 
setInterval(() => { 
	console.log(name); 
}, 100);

只要定时器一直运行,回调函数中引用的 name 就会一直占用内存。垃圾回收程序当然知道这一点,因而就不会清理外部变量。

使用 JavaScript 闭包很容易在不知不觉间造成内存泄漏。请看下面的例子:

let outer = function() { 
     let name = 'Jake'; 
     return function() { 
     	return name; 
     }; 
};

调用 outer()会导致分配给 name 的内存被泄漏。以上代码执行后创建了一个内部闭包,只要返回的函数存在就不能清理 name,因为闭包一直在引用着它。假如 name 的内容很大(不止是一个小字符串),那可能就是个大问题了。

6.静态分配与对象池

为了提升 JavaScript 性能,最后要考虑的一点往往就是压榨浏览器了。此时,一个关键问题就是如何减少浏览器执行垃圾回收的次数。开发者无法直接控制什么时候开始收集垃圾,但可以间接控制触发垃圾回收的条件。理论上,如果能够合理使用分配的内存,同时避免多余的垃圾回收,那就可以保住因释放内存而损失的性能。

浏览器决定何时运行垃圾回收程序的一个标准就是对象更替的速度。如果有很多对象被初始化,然后一下子又都超出了作用域,那么浏览器就会采用更激进的方式调度垃圾回收程序运行,这样当然会影响性能。看一看下面的例子,这是一个计算二维矢量加法的函数:

function addVector(a, b) { 
    let resultant = new Vector(); 
    resultant.x = a.x + b.x; 
    resultant.y = a.y + b.y; 
    return resultant; 
}

调用这个函数时,会在堆上创建一个新对象,然后修改它,最后再把它返回给调用者。如果这个矢量对象的生命周期很短,那么它会很快失去所有对它的引用,成为可以被回收的值。假如这个矢量加法函数频繁被调用,那么垃圾回收调度程序会发现这里对象更替的速度很快,从而会更频繁地安排垃圾回收。

解决方法:

该问题的解决方案是不要动态创建矢量对象,比如可以修改上面的函数,让它使用一个已有的矢量对象

function addVector(a, b, resultant) { 
    resultant.x = a.x + b.x; 
    resultant.y = a.y + b.y; 
    return resultant; 
}

当然,这需要在其他地方实例化矢量参数 resultant,但这个函数的行为没有变。那么在哪里创建矢量可以不让垃圾回收调度程序盯上呢?

1.对象池

一个策略是使用对象池。在初始化的某一时刻,可以创建一个对象池,用来管理一组可回收的对象。应用程序可以向这个对象池请求一个对象、设置其属性、使用它,然后在操作完成后再把它还给对象池。由于没发生对象初始化,垃圾回收探测就不会发现有对象更替,因此垃圾回收程序就不会那么频繁地运行。下面是一个对象池的伪实现:

// vectorPool 是已有的对象池 
let v1 = vectorPool.allocate(); 
let v2 = vectorPool.allocate(); 
let v3 = vectorPool.allocate(); 
v1.x = 10; 
v1.y = 5; 
v2.x = -3; 
v2.y = -6; 
addVector(v1, v2, v3); 
console.log([v3.x, v3.y]); // [7, -1] 
vectorPool.free(v1); 
vectorPool.free(v2); 
vectorPool.free(v3); 
// 如果对象有属性引用了其他对象
// 则这里也需要把这些属性设置为 null 
v1 = null; 
v2 = null; 
v3 = null;
2.静态分配

如果对象池只按需分配矢量(在对象不存在时创建新的,在对象存在时则复用存在的),那么这个实现本质上是一种贪婪算法,有单调增长但为静态的内存。这个对象池必须使用某种结构维护所有对象,数组是比较好的选择。不过,使用数组来实现,必须留意不要招致额外的垃圾回收。比如下面这个例子:

let vectorList = new Array(100); 
let vector = new Vector(); 
vectorList.push(vector);

由于 JavaScript 数组的大小是动态可变的,引擎会删除大小为 100 的数组,再创建一个新的大小为200 的数组。垃圾回收程序会看到这个删除操作,说不定因此很快就会跑来收一次垃圾。要避免这种动态分配操作,可以在初始化时就创建一个大小够用的数组,从而避免上述先删除再创建的操作。不过,必须事先想好这个数组有多大。

注意 静态分配是优化的一种极端形式。如果你的应用程序被垃圾回收严重地拖了后腿,可以利用它提升性能。但这种情况并不多见。大多数情况下,这都属于过早优化,因此不用虑。

四、let const提升性能

ES6增加这两个关键字不仅有助于改善代码风格,而且同样有助于改进垃圾回收的过程。因为 const let 都以块(而非函数)为作用域,所以相比于使用 var,使用这两个新关键字可能会更早地让垃圾回收程序介入,尽早回收应该回收的内存。在块作用域比函数作用域更早终止的情况下,这就有可能发生。

问题

1. call()

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值