一、面向对象
1.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)
1.1.1 普通函数
- 正常调用,不需要 new 关键字
- 执行过程遵循堆栈 + 作用域及作用域链查找的运行机制
1.1.2 构造函数
- 使用 new 关键字调用
- 与普通函数类似,同样会创建私有上下文,然后进栈执行
- 执行 new 操作时,浏览器会创建一个空间表示空以象与 this 关联
- 函数体内如果没有 return 或者说 return 的是基本数据类型,默认返回对象实例
- 函数体内如果返回引用类型,那么就以自己返回为主
- 函数此时叫做类, 返回的结果叫做对象实例
1.1.3 new 操作符
- 正常情况下使用 new 完成对象实例创建,如果当前类不需要传递参数,则可以不加括号运行
- new Foo , 没有加小括号的时候说明 Foo 不需要传参,称之为无参列表
- new Foo 与 new Foo() 的优先级不同,前者为 19, 后者为 20
- 每一次 new 都会将函数重新执行,生成一个新的执行上下文,创建一个新的实例对象,因此两个实例对象不一样
1.2 原型及原型链
1.2.1 名词说明
- prototype 属性
- 每一个函数(除了箭头函数)数据类型, 都自带一个 prototype 属性,指向原型对象(Function 除外)
- 每个原型对象自带一个 constructor 属性,指向当前构造函数本身
- 函数数据类型
- 普通函数、箭头函数、生成器函数
- 构造函数(自定义类)
- 内置函数(内置构造函数)
-
proto 属性
- 每一个对象数据类型,都自带一个 proto 属性(隐式原型)
- 该属性的值指向所属类的原型对象 prototype
- 对象数据类型
- 普通对象,数组对象,正则对象,日期对象
- prototype 原型对象
- 实例对象
- 函数本身也是对象
- Object 类
- 所有对象都是 Object 内置类的实例
- Object 也是一个函数, 同样具有 prototype 属性,指向自己的原型对象
- 它的原型也是一个对象, 因此具有 proto 属性
- Object 原型对象的 proto 指向 Null (内部设计)
1.2.2 原型链查找机制
- 首先查找自己的私有属性,私有中如果存在则使用私有
- 私有中如果不存在,则默认基于 proto 找所属类的原型对象
- 如果原型对象上没有,则基于原型对象的 proto 继续向上查找,直到找到 Object.prototype 为止
1.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()
1.3 重写 new 方法
1.3.1 new 做了什么
- 创建实例对象
- 执行构造函数, 将 this 指向实例对象
- 处理返回值
1.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('syy')
// 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 p2 = _new(Person, 'abc')
p2.slogan()
p2.sayName()
console.log(p2 instanceof Person)
1.4 Function 与 Object
1.4.1 函数多种角色
- 函数
- 普通函数调用(堆栈执行及作用域)
- 构造函数实例(原型及原型链)
- 对象
- 键值对
- 三种角色之间没有必然的联系,但是最核心的理念是函数就是函数,函数是一等公民
1.4.2 经典语录
- Function 是一等公民,在 JS 中存在多种角色,普通函数, 构造函数,对象
- 每一个对象都存在 --proto-- 属性, 指向所属类的原型对象(隐式原型,原型链属性)
- 每一个函数都存在 prototype 属性, 指向它的原型对象
- 所有函数都是 Function 内置类的实例,且 Function 本身也是一个函数
- 所有对象都是 Object 的实例,且 Object 本身也是一个函数
- Function 与 Object 是二大并行的基类,虽然最终查找落脚点都是 Object 身上
- Function.prototype 原型对象是一个匿名函数,但虽然它是一个函数,它的处理机制和原型对象是一样的,它的 --proto-- 属性指向所属类的原型对象,也就是 Object.prototype
1.4.3 无 prototype 属性
- Function.prototype 原型是一个匿名函数,但是它没有 prototype 属性
- 对象中使用 ES6 语法定义函数
const obj = {say(){}}
, say方法不具备 - 箭头函数
- 不具备 prototype 属性的函数是不能执行 new 操作的
1.5 This 规律
在浏览器平台下运行 JS , 非函数当中的 this 一般都指向 window
因此这里讨论的是函数执行过程中的 this
需要注意:ES6+ 的箭头函数中是没有自己 this 的,处理机制就是使用上下文当中的 this
1.5.1 this 是什么
- this 就是当前函数执行的主体(谁执行了函数), 不等于执行上下文或者当前作用域
- syy 在拉勾教育讲前端
- 讲前端是一个动作(函数)
- 拉勾教育(执行上下文)
- syy 主体,本次函数在当前执行上下文中执行的 this 指向
1.5.2 常见 this 场景
- 事件绑定
- 普通函数
- 构造函数
- 箭头函数
- 基于 call bind apply 强制改变 this 指向
1.5.3 规律
- 事件绑定
- 不论是 DOM2 还是 DOM0 级事件绑定,事件触发时 this 一般都是被操作的元素
- 普通函数
- 函数执行时查看前端是否有点,如果有点,则点前面的就是执行主体, 没有点就是 window , 严格模式下是 undefined
- 特殊情况
- 匿名函数中的 this 是 window 或者 undefined
- 回调函数中的 this 一般也是 window 或者 undefined
- 小括号语法
- 如果小括号只有一项,则相当于没加
- 如果小括号当中有多项,则取出最后一项,此时相当于拷贝函数,所以调用时主体是 window
1.5.4 练习
(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)
二、异步编程与事件环
2.1 名词说明
2.1.1 进程与线程
- 进程可以看做是一个应用程序(例如打开浏览器或者浏览器打开一个界面)
- 线程是程序当中具体做事情的人, 每个线程同一时刻只能做一件事情
- 一个进程当中可以包含多个线程
2.1.2 同步与异步
- 同步编程:一件一件的做事情,上一件没有做完,下一件事情不会被处理(单线程)
- 异步编程:上一件事情没有处理完,可以直接处理下一件事情(多线程)
- JS 基于单线程的 EventLoop 机制也可以实现异步编程
2.1.3 JS 中异步操作
- promise(then)
- aync await (generator)
- requestAnimationFrame
- 定时器操作
- ajax 网络请求
- 事件绑定
2.1.4 JS 单线程
浏览器平台下的 JS 代码是由 JS 引擎执行的, 所以它是单线程的
浏览器是多线程: GUI 渲染线程、JS引擎线程、事件触发线程、定时器触发线程、异步 http请求线程
- JS 中大部分代码都是同步编程
- JS 可以基于单线程的 EventLoop (事件循环机制) 实现出异步效果
2.2 EventLoop 模型
2.2.1 代码执行顺序
- 浏览器加载界面之后会开启一个线程来执行 JS,称之叫 JS引擎(主线程)
- JS引擎会自上而下的执行 JS 代码,此过程会遇到(定时器,网络请求,事件绑定,promise....)
- 遇到上述代码执行之后,浏览器会开启一个 Event Queue(任务|事件)队列 优先级队列结构
- 在队列当中存在二个任务队列:微任务 microtask | 宏任务 macrotask
- 最终会将遇到的异步任务存放到 Event Queue 队列当中(未执行)
- 主线程会继续向下执行同步代码,直到所有同步执行完就会处理异步任务
- 进入 Event Queue 当中查找异步任务,找到之后放入主线程中执行(此时主线程又被占用)
- 执行完一个异步任务之后,主线程再次空闲,此时再进入 Event Queue 查找余下的异步任务
2.2.2 异步任务执行顺序
- 先执行微任务(只要有微任务就不会处理宏任务)
- 微任务(一般是谁先放置谁先执行)
- 宏任务(一般是谁先到达的谁先执行)
2.2.3 整体顺序
- 同步任务
- 异步微任务
- 异步宏任务
- 如果同步任务执行过程中遇到可执行的异步任务,此时依然需要等到同步任务执行完
- 如果同步任务执行完,还没有可执行的异步任务,此时也只能等待
- 不论何时放入的微任务,只要异步微任务存在,就永远不会执行异步宏任务
2.2.4 setTimeout
- 设置定时器这个操作是同步的
- 放置在 Event Queue 中的任务是异步宏任务
- 函数调用返回数字,表示当前是第几个定时器
- 等待时间设置为 0 时,也不是立即执行,浏览器存在最快的反应时间
- 定时器的等待时间到了之后,它的代码可能还不会执行(处于异步队列中,同步任务还未完成执行)
2.2.5 练习
setTimeout(() => {
console.log('1')
}, 30)
console.log(2)
setTimeout(() => {
console.log(3)
}, 20)
console.log(4)
console.time('AA')
// 消耗95ms
for (let i = 0; i < 88888888; i++) { }
console.timeEnd('AA')
console.log(5)
setTimeout(() => {
console.log(6)
}, 18)
console.log(7)
setTimeout(() => {
console.log(8)
}, 25)
console.log(9)
async function async1() {
console.log('async1 执行了')
await async2()
console.log('async1 结束了')
}
async function async2() {
console.log('async2')
}
console.log('同步代码执行了')
setTimeout(() => {
console.log('setTimeout')
}, 0)
async1()
new Promise((resolve) => {
console.log('promise1')
resolve()
}).then(() => {
console.log('promise2')
})
console.log('同步代码结束了')
function fn1() {
console.log('fn1 start')
return new Promise((resolve) => {
resolve('zcegg1')
})
}
function fn2() {
console.log('fn2 start')
return new Promise((resolve) => {
setTimeout(() => {
resolve('zcegg2')
}, 10)
})
}
console.log('a')
setTimeout(async () => {
console.log('b')
await fn1()
console.log('c')
}, 20)
for (let i = 0; i < 88888888; i++) { } // 90ms
console.log('d')
fn1().then((result) => {
console.log('f')
})
fn2().then((result) => {
console.log('g')
})
setTimeout(() => {
console.log('h')
}, 0)
console.log('end')