一、JS堆栈执行
1.1 练习题
var l = { x: 10 }
var g = l
l.y = l = { x: 200 }
console.log(l.y)
console.log(g)
1.2 相关名词
分析代码执行时会使用到的相关概念
- JS 执行平台:
- 不同浏览器
- Nodejs
- webview
- 不论是哪一种平台都需要具备代码执行的环境
- 执行环境
- 不论何种语言编写的代码,最终执行都是发生在内存中
- JS代码执行时会生成一个栈空间,它的本质也是内存空间
- 每当浏览器加载界面时就会从计算机内存中申请一片空间,称之为执行环境栈
- ESC(execution stack context)
- 执行上下文
- 一个 JS 文件会包含多行代码,不同行代码组合在一起就是代码块
- 多个代码块中的代码直接放入栈中运行,必须会出现互相干扰语法冲突
- 每个代码块都有自己的执行上下文,在上下文中保存了当前段代码执行时所需要数据
- 执行上下文使用 EC(execution context) 表示
- 进栈执行
- 执行环境栈是一个先进后出的栈结构
- 代码运行时会产生不同的执行上下文
- 不同上下文进栈执行,执行代码,最终决定是否出栈
- EC(G)
- execution context global,全局执行上下文,浏览器加载界面时默认创建
- VO(G)
- variable object,全局变量对象,用于存放全局上下文当中声明定义的变量
- GO
- global object,全局对象,它和 VO 并不是一个东西,在浏览器平台下我们可以认为是
window - 做为一个对象,它同样占据空间,浏览器加载界面时就会创建,在它内部保存了许多 JS 可以
直接调用的内容 - 例如 setInterval setTimeout JSON 等
- 为了方便使用上述的 API, 在 VO(G) 当中就创建了一个 window 属性指向当前的空间
- 声明:采用具体的关键字声明一个变量 var let const function var name
- 定义:定义就是给某一个变量执行赋值 name = ‘拉勾教育’
1.3 堆栈中的基本值
var l = 100
var g = l
g = 101
console.log(l)
/*
01 浏览器会开启一个线程专门用于执行 JS 代码,同时申请空间做为执行环境栈
02 浏览器加载界面时会创建EC(G) 全局执行上下文,然后进执行代码
03 全局执行上下文当中会存在 VO(G), 用于保存当前上下文中的数据
04 代码执行前会存在变量提升,var声明的变量在提升阶段只声明不定义
*/
总结
- 浏览器加载界面的时候默认会创建执行环境栈、全局执行上下文、GO
- EC(G) 内部会有 VO(G) 专门用于存放当前上下文当中的数据
- EC(G) 上下文会在浏览器关闭之后执行出栈,释放掉相应的空间
- 基本数据类型值存放在栈空间当中
- 作用域链查找,代码运行时使用到了某个变量,首先会在当前上下文中查找,如果没有则向
上,直到 GO
1.4 堆栈中的引用类型
var l = { x: 17 }
var g = l
g['y'] = 100
console.log(l.y)
-------------------------------------------------
var l = { x: 10 }
var g = l
g = { y: 100 }
console.log(l.x)
总结
基本数据类型(原始值)存放在栈内存空间中,引用类型存放在堆内存空间中
每个堆内存空间都会有一个16进制地址
在栈区当中存放的就是能找到某个堆内存的 16 进制地址
1.5 堆栈中的函数
函数本身也是对象
var lg = [88, 100]
function foo(obj) {
obj[0] = 100
obj = [100]
obj[1] = 200
console.log(obj)
}
foo(lg)
console.log(lg)
1.5.1 函数创建
变量提升阶段对于函数来说是既声明又定义
函数的创建和变量的提升类似,可以将函数名看做是一个变量名,不同的就是包含了声明+定义
函数也是一个对象,因此它同样在堆中存储,然后将内存地址存放在栈区
对于函数来说,声明和定义都是发生在提升阶段,因此代码执行时看到了function foo(){} 这种代码
后一般是不操作的
函数在创建的时候就确定了作用域,也就是当前的执行上下文(重要)
在创建函数的时候它的内存当中存放的是字符串形式的函数体
1.5.2 函数执行
- 函数执行的目的就是为了将内存当中存储的字符串形式的代码真正的运行起来
- 代码运行时需要保证当前代码段与其它上下文中的代码段相互隔离,所以函数每次执行都会生成一
个执行上下文
- 确定作用域链
- 确定函数中的 this
- 初始化 arguments
- 形参赋值
- 变量提升
- 函数代码执行
函数执行时如果有形参赋值,那么就会在 AO(G)当中新增属性
1.6 闭包机制
1.6.1 闭包含义
闭包是一种机制,代码只是具体的表现形式,例如我们常说的大函数嵌套小函数,再返回一个小函
数
保护 保存
函数执行时会产新生成一个执行上下文,一般来说函数中的代码执行结束之后就需要出栈从而释放
当前上下文所占据的内存空间,从而释放它内部的声明和值,但是如果此时当前执行上下文当中的
数据(一般就是堆内存引用)被当前上下文之外的变量所引用,那么这个上下文就不能被释放掉,
此时就形成了一个闭包
闭包的好处就是可以对一些数据进行保存,例如下文中的 zce ,函数内部的 zce和全局的 zce 互不干扰,
同时闭包还可以保存数据,例如 0x001 所对应的内存空间,本该在 FN 执行结束后释放掉,但是由于
EC(G)当中的 foo 对其有引用,所以让它可以在后续代码中继续被使用
var zce = 100
function fn() {
var zce = 200
return function (a) {
console.log(a + zce++)
}
}
var foo = fn()
foo(10)
foo(20)
1.6.2 闭包与垃圾回收
- 上述代码运行可以发现,代码的运行是需要内存空间的。无论是栈内存还是堆内存都属于计算机内
存空间 - 内存空间大小是有上限的,因此不能无限制使用,所以需要内存管理,也就是垃圾回收
1、. 以chrome 为例,它会在空闲时间执行垃圾回收操作,完成内存空间的回收
- 栈内存
1、主要用于存储基本数据类型值
2、当某一个上下文执行结束之后,如果它内部的空间没有再被其它人使用,那么就会释放 掉这部分空间完成回收
- 堆内存
1、用于存放引用数据类型
2、如果A上下文中的堆内存在 A 中代码执行完成之后,仍然被 B 上下文所引用,那么这个堆内存及A上下文所占用的空间就无法被释放掉,也就是常说的闭包,如果这样的地方多 了那么对于性能就是一种消耗
- 依据选择在合适的地方主动将变量定义为 null ,释放掉某些引用
- EC(G):全局执行上下文是在浏览器加载界面的时候就创建的,因此界面不关闭这部分执行上 下文是不会被回收的
1.6.3 闭包练习
// let m = 5
// function foo(m) {
// return function (n) {
// console.log(n + (++m))
// }
// }
// let fn = foo(8)
// fn(10)//19
// foo(11)(13)//25
// fn(20)//30
// console.log(m)//5
// let m = 10,
// n = 10
// function foo(m) {
// foo = function (n) {
// console.log(m + n++)
// console.log(111);
// }
// console.log(222);
// console.log(m++)
// }
// foo(5)//5
// foo(7)//13
// function fun(n, o) {
// console.log(o)
// return {
// fun: function (m) {
// return fun(m, n)
// }
// }
// }
// var c = fun(10).fun(3)
// c.fun(6)
// c.fun(8)
二、面向对象
2.1 普通函数与构造函数
函数还是之前的函数,唯一的区别就是首字母大写
function Foo(m, n) {
let ret = m + n
this.m = m
this.n = n
return ret
}
// 01 普通函数调用
let ret = Foo(10, 20)
console.log(ret)
// 02 构造函数执行
let res = new Foo(20, 20)
console.log(res)
2.1.1 普通函数
- 正常调用,不需要 new 关键字
- 执行过程还是按着堆栈执行 + 作用域链查找机制
2.1.2 构造函数
- 使用 new 关键字调用
- 与普通函数类似,同样会创建私有上下文,然后进栈执行
- 执行 new 操作时,浏览器会创建一个空间表示空对象与this 进行关联
- 函数体内如果没有 return 或者说 return 的是基本数据类型,默认返回对象实例
- 函数体内如果返回引用类型,那么就以自己返回为主
- 函数此时叫做类,返回的结果叫对象实例
2.1.3 new 操作符
- 正常情况下使用 new 完成对象实例创建,如果当前类不需要传递参数,则可以不加括号运行
- new Foo,未加小括号说明 FOO不需要传参,称之为无参列表
- new Foo 与 new Foo() 的优先级不同,前者为 19, 后者为 20
- 每一次 new都会将函数重新执行,生成一个新的执行上下文,创建一个新的实例对象,因此两个 实例对象不一样
2.2 原型及原型链
2.2.1 名词说明
prototype 属性
- 每一个函数(除箭头函数)数据类型,都自带一个 prototype 属性,指向原型对象
(Function除外) - 每个原型对象自带一个 constructor 属性,指向当前构造函数本身
- 函数数据类型
1、普通函数、箭头函数、生成器函数
2、构造函数(自定义类)
3、内置函数(内置构造函数)
proto 属性
- 每一个对象数据类型,都自带一个 proto 属性,(隐式原型)
- 该属性的值指向所属类的原型对象 prototype
- 对象数据类型
1、普通对象、数组对象、正则对象、日期对象
2、prototype 原型对象
3、实例对象
4、函数也是对象
Object 类
- 所有对象都是 Object 内置类的实例
- Object也是一个函数,同样具有 prototype 属性,指向自己的原型对象
- 它的原型也是一个对象,因此具有 proto 属性
- Object 原型对象的proto 指向 Null( 内部设计 )
2.2.2 原型链查找机制
- 首先找自己私有的属性, 私有中存在就是私有的
- 私有中不存在,则默认基于 proto 找所属类的原型对象
- 如果类的原型上没有,则基于原型对象的 proto 继续向上查找,直到找到 Object.prototype 为止
2.2.3 示例代码
// function Foo() {
// this.m = 10
// this.n = 24
// this.getM = function () {
// console.log(this.m)
// }
// }
// Foo.prototype.getM = function () {
// console.log(this.m)
// }
// Foo.prototype.getN = function () {
// console.log(this.n)
// }
// let foo1 = new Foo
// let foo2 = new Foo
// console.log(foo1.getM === foo2.getM)
// console.log(foo1.getN === foo2.getN)
// console.log(foo1.__proto__.getN === Foo.prototype.getN)
// console.log(foo1.__proto__.getM === foo2.getM)
// console.log(foo1.getM === Foo.prototype.getM)
// console.log(foo1.constructor)
// console.log(Foo.prototype.__proto__.constructor)
// foo1.getM()
// foo1.__proto__.getM()
// foo2.getN()
// Foo.prototype.getN()
2.3 重写 new 方法
2.3.1 new 做了什么
- 创建实例对象
- 执行构造函数,将this指向实例对象
- 处理返回值
2.3.2 模拟new实现
function Person(name) {
this.name = name
}
Person.prototype.slogan = function () {
console.log('前端界最帅的人')
}
Person.prototype.sayName = function () {
console.log(`我的名字是${this.name}`)
}
// let p1 = new Person('zce')
// p1.slogan()
// p1.sayName()
function _new(Ctor, ...params) {
//01 创建实例对象
// let obj = {}
// obj.__proto__ = Ctor.prototype
let obj = Object.create(Ctor.prototype)
//02 调用构造函数,改变this指向
let ret = Ctor.call(obj, ...params)
//03 处理返回结果
if (ret !== null && /^(object|function)$/.test(typeof ret)) return ret
return obj
}
let p1 = _new(Person, 'zce')
p1.slogan()
p1.sayName()
console.log(p1 instanceof Person)
2.4 Function 与 Object
2.4.1 函数多种角色
- 函数
- 普通函数调用(堆栈执行作用域)
- 构造函数实例化(原型及原型链)
- 对象
- 键值对
- 三种角色之间没有必然的联系,但是最核心的函数就是函数
2.4.2 语录
- Function是一等公民,在 JS中存在多种角色,普通函数、构造函数、对象
- 每一个对象都存在 proto 属性,指向所属类的原型对象(隐式原型,原型链属性)
- 每一个函数都存在 prototype 属性,指向它的原型对象
- 所有函数都是 Function 内置类的实例,且Function 本身也是一个函数
- 所有对象都是 Object 的实例,且 Object 本身也是一个函数
- Function 与 Object 是二大并行的基类,虽然最终查找落脚点都是 Object 身上
- Function.prototype 原型对象是一个匿名函数,虽然它是一个函数,但是它的处理机制和原型对象
是一样的, 它的 proto 属性指向所属类的原型对象,也就是 Object.prototype
2.4.3 原型链图
2.4.4 不具备prototype 属性
- Function.prototype 不具备,是一个匿名函数
- 对象中使用ES6语法定义函数 const obj = { say(){} }
- 箭头函数
- 不具备prototype属性的函数是不能执行 new 操作的
2.5 This规律
在浏览器平台下运行 JS ,非函数当中的this 一般都指向 window。
因此这里讨论的是函数执行过程中的 this
需要注意在 ES6+ 的箭头函数中是没有自己this的,处理机制是使用自己上下文里的 this
2.5.1 This是什么
- this 就是当前函数执行的主体(谁执行了函数),不等于执行上下文,当前作用域
- zce 在拉勾教育讲前端
- 讲前端是一个动作(函数)
- 拉勾教育(执行上下文)
- zce 主体, 本次函数在当前执行上下文执行的 this 指向
2.5.2 常见this场景
- 事件绑定
- 普通函数
- 构造函数
- 箭头函数
- 基于 call/bind/apply 强制改变 this 指向
2.5.3 规律
- 事件绑定
- 不论是DOM2还是 DOM0 事件绑定,事件触发时this一般都是被操作的元素
- 普通函数
- 函数执行时查看前面是否有点,如果有点,则点前面的就是执行主体,没有点就是 window,
严格模式下是 undefined - 特殊情况
- 匿名函数中的 this 是 window 或者 undefined
- 回调函数中的 this 一般也是window或者 undefined
- 小括号语法
- 如果小括号只有一项,则相当于没加
- 如果小括号当中有多项,则取出最后一项,此时相当于拷贝函数,所以调用时主体
是 window
2.5.3 this 练习
(function () {
console.log(this)
})()
let arr = [1, 3, 5, 7]
obj = {
name: '拉勾教育'
}
arr.map(function (item, index) {
console.log(this)
}, obj)
? 普通函数调用
let obj = {
fn: function () {
console.log(this, 111)
}
}
let fn = obj.fn;
fn() // window
obj.fn(); // obj
(10, fn, obj.fn)();
var a = 3,
obj = {
a: 5
}
obj.fn = (function () {
this.a *= ++a
return function (b) {
this.a *= (++a) + b
console.log(a)
}
})();
var fn = obj.fn
obj.fn(6)
fn(4)
console.log(obj.a, a)