文章中若有错误的地方,欢迎指出。
我们先来看看下面👇几道面试题:
// example1
let a = {}, b = '0', c = 0;
a[b] = '堆栈';
a[c] = '内存';
console.log(a[b]);//内存
-----------------------------------------------------------------
// example 2
let a = {},
b = Symbol('1'),
c = Symbli('2');
a[b] = '堆栈';
a[c] = '内存';
console.log(a[b]);//堆栈
//考察:对象属性名的操作,如果是一个对象,会变成字符串[Object,Object]
let a = {},
b = {
n: '1'
},
c = {
m: '2'
};
a[b] = '珠峰';
a[c] = '培训';
console.log(a[b]);
//=>可能二问:Object.prototype.toString / valueOf
----------------------------------------------------------------
// example 3
var test = (function(i){
return function(){
alert(i *= 2);
}
})(2)
test(5)//'4',注意alert弹出的是字符串'4';console输出的是4
-----------------------------------------------------------------
// example 4
var a = 0;
b = 0;
function A(a) {
A = function (b) {
alert(a + b++);
}
alert(a++);
}
A(1); // '1'
A(2); // '4'
堆栈内存
- 堆: 存储引用类型值的空间。
堆是堆内存的简称,自动分配内存,内存大小不一,也不会自动释放。 引用数据类型:Symbol、BigInt、Object、String这样的数据存在堆内存中,但是数据指针是存放在栈内存中的,当我们访问引用数据时,先从栈内存中获取指针,通过指针在堆内存中找到数据。
- 栈:存储基本类型值和指定代码的环境
栈是栈内存的简称,自动分配相对固定大小的内存空间,并由系统自动释放。
基本数据类型:Undefined,Null,Boolean,Number都是直接按值存在栈中,每种类型的数据占用的内存空间大小都是固定的,并且由系统自动分配自动释放。
特别注意字符串是保存在堆中的,这里用字符串做个简单的推理:
- 第一:栈里的值发生改变时,JavaScript引擎是直接修改内存中的值,而字符串在JavaScript中是一个不可变的值,也就是说新的字符串替换旧的字符串时,如果在栈中是无法操作的。
var s = 'abc'
var a = 111
s = 'bcd' // 如果这时候在栈中重新开辟内存保存bcd,再把新的内存地址赋值给s,那么s和a在栈中的相对位置就会发生改变
- 第二:字符串的长度肯定是超过32位的,而当它的长度超过32位时,栈内存的一个单元是存储不了的。
- 另外有一点,其实在v8引擎中,对值得驻留的字符串内存中相同的字符串只会保存一份,值得驻留的字符串指的是:在有些场景下会重复出现的字符串,当两个变量保存相同的字符串时,它们实际保存了这个字符串在内存中的地址。这叫做字符串驻留(String Interning)
example1 中定义了a为一个对象,对于引用类型我们使用堆内存来处理。
在浏览器中会先创建一个变量a, 然后给a分配一个16进制的堆内存空间。
接着给变量b和变量c赋值。再往下执行a[b],a[c],这里要注意,
对一个对象来说,属性名是不能重复的,一般是一个字符串。数字属性名和字符串属性名是相等的。 所以例子中数字0 和字符串'0' 都代表0,a[c]会覆盖a[b]的值。即输出结果为'内存'。
对象的属性名还可以是布尔值、null、undefined、Symbol;特殊的就是数字和字符串,运行的时候会转换成字符串。
可以看到下面示例中,无论
a[b]还是a[c],他的属性名都是字符串[Object,Object],后面的会覆盖前面的值,输出为'培训'
Symbol
example2 中,对象的属性名不一定只是字符串,还有symbool。
Symbol的特点就是可以创建一个独一无二的值(但并不是字符串)
可以在控制台中尝试:
Symbol('1') === Symbol('1') // false
执行上下文(execution context)
example3 中,浏览器一加载页面,就形成了一个栈内存。栈内存运行代码过程中,当每一个函数执行的时候,都会形成一个全新的执行栈(执行上下文Excuation Context Stack),放到栈中去执行。
var test = AAAFFF111创建了一个函数且立即执行了(正常情况是先创建一个堆,然后把堆执行,执行后将返回的结果给test)。
代码中return一个函数,也即return一个16进制的地址(图中AAAFFF111) 当前函数的上级作用域是属于当前这个执行上下文所在的作用域。
正常是创建一个堆。
执行上下文在代码块执行前创建,作为代码块运行的基本执行环境。JavaScript中有三种可执行代码块:
- 函数代码块(Function code)
- 全局代码块(Global code)
- eval代码块(Eval code)
这三种可执行代码块对应着三种执行上下文:
- 全局执行上下文 : 这是基础上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
- 函数执行上下文 : 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建。
- Eval 执行上下文 : 执行在 eval 内部的代码也会有它属于自己的执行上下文,除非你想搞黑魔法,不然不要轻易使用它。
执行上下文分为两个阶段:
- 创建阶段
- 执行阶段
这两个阶段具体做了些什么在这里笔记了,具体可阅读《JavaScript的运行机制》
闭包
example4 中,A(1)执行,会形成一个全新的执行上下文ECStack(AAAFFF000),
形参a赋值为1,让A = function B(也会形成一个堆BBBFFF000,存一些代码字符串),js中是严格区分大小写的,AAAFFF000中a是自己的私有的值为1,而A不是自己私有的,则往上级作用域找,即全局下的A (往上级作用找,找到A,并把AAAFFF000改为BBBFFF000,从而重写了全局方法A指向),此时闭包则形成了。
接下来执行aler(a++) => a = 2
a++ 先和别的运算,再自身累加1
++a 先自身累加1,再和别的运算
接着执行A(2),此时全局的A = BBBFFF000,形参赋值为2,执行alert(a + b++),这里先执行a+b,再执行b++,最终A(2)输出为'4'。
闭包是什么?
MDN的解释:闭包是函数和声明该函数的词法环境的组合。
可以理解为:闭包 = 【函数】和 【函数体内可访问的变量总和】。
举个简单的例子:
(function() {
var a = 1;
function add() {
var b = 2
var sum = b + a
console.log(sum); // 3
}
add()
})()
add函数本身,以及其内部可访问的变量,即a = 1,这两个组合在一起就称为闭包,仅此而已。
先看看作用域链是怎么形成的?
JavaScript属于静态作用域,即声明的作用域是根据程序正文在编译时就确定的,有时也称为词法作用域。
其本质是JavaScript在执行过程中会创造可执行上下文,可执行上下文中的词法环境中含有外部词法环境的引用,我们可以通过这个引用获取外部词法环境的变量、声明等,这些引用串联起来一直指向全局的词法环境,因此形成了作用域链。
所以,闭包是怎么形成的?
可执行上下文种的词法环境中含有外部词法环境的引用,我们可以通过这个引用获取外部词法环境的变量、声明等,因此形成了闭包。
闭包的作用
闭包最大的作用就是隐藏变量,闭包的一大特性就是内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。
基于此特性,JavaScript可以实现私有变量、特权变量、储存变量等。
这里只以私有变量举例
私有变量的实现方法很多:有靠约定的(变量名前加_),有靠Proxy代理的,也有靠Symbol这种新数据类型的。
但是真正广泛流行的其实是使用闭包。
function Person(){
var name = 'cxk';
this.getName = function(){
return name;
}
this.setName = function(value){
name = value;
}
}
const cxk = new Person()
console.log(cxk.getName()) //cxk
cxk.setName('jntm')
console.log(cxk.getName()) //jntm
console.log(name) //name is not defined
函数体内的var name = 'cxk'只有getName和setName两个函数可以访问,外部无法访问,相对于将变量私有化。