js视频学习记录

122 阅读15分钟

视频是17年的,文章概念可能存在问题,待更正!!!

原型与原型链

原型

  1. 函数的prototype属性 每个函数都有一个prototype属性,它默认指向一个Object空对象(即称为原型对象)。
    原型对象中有一个属性constructor,它指向函数对象。

  2. 给原型对象添加属性(一般都是方法) 作用:函数的所有实例对象自动拥有原型中的属性(方法)

console.log(Date.prototype, typeof Date.prototype)

function fun() {

}
fun.prototype.test = function() {
    console.log('test')
}
console.log(fun.prototype) // 默认指向一个Object空对象(没有我们自定义的属性)

// 原型对象中有一个属性constructor,它指向函数对象
console.log(Date.prototype.constructor === Date) // true
  1. 显式原型与隐式原型 每个函数function都有一个prototype,即显式原型:每个实例对象都有一个__proto__,可称为隐式原型。 对象的隐式原型的值为其对应构造函数的显式原型的值。 内存结构:

image.png

总结:
函数的prototype属性:在定义函数时自动添加的,默认值是一个空Object对象
对象的__proto__属性:创建对象时自动添加的,默认值为构造函数的prototype属性

// 显式原型
function Fn() {}
console.log(Fn.prototype)

// 隐式原型
var fn = new Fn()
console.log(fn.prototype)

// 对象的隐式原型的值为其对应构造函数的显式原型的值
console.log(Fn.prototype === fn.__proto__) // true

原型链

原型链别名隐式原型链,它的作用是查找对象的属性(方法)。

访问一个对象的属性时,先在自身属性中查找,找到返回;
如果没有,再沿着__proto__这条链上查找,找到返回;
如果最终没有找到,返回undefined。

构造函数/原型/实体对象的关系:

image.png

1、函数的显式原型指向的对象默认是空Object实例对象(但Object不满足)
2、所有的函数都是Function的实例(包括Function)
3、Object的原型对象是原型链尽头

属性问题

1、读取对象的属性值时会自动到原型链中查找
2、设置对象的属性值时不会查找原型链,如果当前对象中没有此属性,直接添加此属性并设置其值
3、方法一般定义在原型中,属性一般通过构造函数定义在对象本身上

function Fn() {}
Fn.prototype.a = 'xxx'
var fn1 = new Fn()
console.log(fn1.a, fn1)

var fn2 = new Fn()
fn2.a = 'yyy'
console.log(fn1.a, fn2.a) // xxx  yyy

instanceof

1、instanceof是如何判断的?
表达式:A instanceof B---如果B函数的显式原型对象在A的原型链上,返回true,否则返回false
2、Function是通过new自己产生的实例

function Foo() {}
var f1 = new Foo()
console.log(f1 instanceof Foo) // true
console.log(f1 instanceof Object) // true

image.png

// 实例对象的隐式原型等于构造函数的显式原型
console.log(Object instanceof Function) // true
console.log(Object instanceof Object) // true
console.log(Function instanceof Function) // true
console.log(Function instanceof Object) // true

function Foo() {}
console.log(Object instanceof Foo) // false

测试1

function A() {}
A.prototype.n = 1

var b = new A()
A.prototype = {
    n: 2,
    m:3
}
var c = new A()
console.log(b.n, b.m, c.n, c.m) // 1 undefined 2 3

测试2

var F = function() {}
Object.prototpe.a = function() {
    console.log('a')
}
Function.prototype.b = function() {
    console.log('a')
}
var f = new F()
f.a() // a()
f.b() // 报错 
F.a() // a()
F.b() // b()

执行上下文和执行上下文栈

变量提升与函数提升

1、变量声明提升
通过var定义声明的变量,在定义语句之前就可以访问到,值为undefined
2、函数声明提升
通过function声明的函数,在之前就可以执行调用,值为函数定义(对象)
3、变量提升和函数提升是如何产生的?

console.log(b) // 变量提升
fn2() // 可调用 函数提升
// fn3() // 不能调用,此时为变量提升
var b = 3

function fn2() {
    console.log('fn2')
}

var fn3 =  function () {
    console.log('fn3')
}

执行上下文

1、代码分类(根据位置):全局代码、函数代码
2、全局执行上下文

  • 在执行全局代码前将window确定为全局执行上下文
  • 对全局数据进行预处理
    • 对var定义的全局变量==>undefined,添加为window的属性
    • function声明的全局函数==>赋值(fun),添加为window的方法
    • this==>赋值(window)
  • 开始执行全局代码
console.log(a1, window.a1)
a2()
console.log(this)

var a1 = 3
function a2() {
    console.log('a2()')
}
console.log(a1)

3、函数执行上下文

  • 在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象(虚拟的),存在于栈中
  • 对局部数据进行预处理
    • 形参变量==>赋值(实参)==>添加为执行上下文的属性
    • arguments==>赋值(实参列表),添加为执行上下文的属性
    • var定义的局部变量==>undefined,添加为执行上下文的属性
    • function声明的函数==>赋值(fun),添加为执行上下文的方法
    • this==>赋值(调用函数的对象)
  • 开始执行函数体代码
function fn(a1) {
    console.log(a1) // 2
    console.log(a2()) // undefined
    a3() // a3()
    console.log(this) // window
    console.log(arguments) // 伪数组[2,3]
    
    var a2 = 3
    function a3() {
        console.log('a3()')
    }
}
fn(2,3)

执行上下文栈

1、在全局代码执行之前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象
2、在全局执行上下文(window)确定后,将其添加到栈中(压栈)
3、在函数执行上下文创建后,将其添加到栈中(压栈)
4、在当前函数执行完后,将栈顶的对象移除(出栈)
5、当所有的代码执行完后,栈中只剩下window

// 1.进入全局执行上下文
var  a = 10
var bar = function(x) {
    var b = 5
    foo(x + b) // 3.进入foo执行上下文
}
var foo = function(y) {
    var c = 5
    console.log(a + c + y)
}
bar(10) // 2. 进入bar函数执行上下文
console.log('gb:' + i)
var  i = 1
foo(1)
function foo(i) {
  if (i == 4) return
  console.log('fb:' + i)
  foo(i + 1)
  console.log('fe:' + i)
}
console.log('ge:' + i)

/*
1.输出
gb:undefined
fb:1
fb:2
fb:3
fe:3
fe:2
fe:1
ge:1
2. 整个过程中产生了几个执行上下文--5个
*/

image.png

// 先执行变量提升,再执行函数提升
function a() {}
var a;
console.log(typeof a) // function

if (!(b in window)) {
    var b = 1
}
console.log(b) // undefined

var c = 1
function c(c) {
    console.log(c)
}
c(2) // 报错--c is not function
/*
var c
function c(c) {
    console.log(c)
}
c = 1
c(2)
*/

作用域与作用域链

1、理解:就是一块“地盘”,一个代码段所在的区域,它是静态的(相对于上下文对象),在编写代码时就确定了。
2、分类
全局作用域
函数作用域
没有块作用域(ES6有了)
3、作用:隔离变量,不同作用域下同名的变量不会有冲突

var a = 10
b = 20
function fn(x) {
  var a = 100
  c = 300
  console.log('fn()', a, b, c, x) // 100 20 300 10
  function bar(X) {
    var a = 1000
    d = 400
    console.log('bar()', a, b, c, d, X)
    // // 1000 20 300 400 100 // 1000 20 300 400 200
  }
  bar(100)
  bar(200)
}
fn(10)

image.png

作用域与执行上下文

1、区别

  • 区别1
    • 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了,而不是在函数调用时
    • 全局执行上下文环境是在全局作用域确定之后,js代码马上执行之前创建
    • 函数执行上下文是在调用函数时,函数体代码执行之前创建
  • 区别2
    • 作用域是静态的,只要函数定义好了就一直存在,且不会变化
    • 上下文环境是动态的,调用函数时创建,函数调用结束时上下文环境就会被自动释放 2、联系
  • 执行上下文环境(对象)是从属于所在的作用域
  • 全局上下文环境==>全局作用域
  • 函数上下文环境==>对应的函数作用域

作用域链

1、理解
多个上下级关系的作用域形成的链,它的方向是从下向上的(从内到外)
查找变量时就是沿着作用域链来查找的
2、查找变量的规则
在当前作用域下的执行上下文中查找对应的属性,如果有则直接返回,否则进入2
在上一级作用域的执行上下文中查找对应的属性,如果有就直接返回,否则进入3
再次执行2的相同操作,直到全局作用域,如果还找不到就抛出找不到的异常

var x = 10
function fn() {
    console.log(x)
}
function show(f) {
    var x = 20
    f()
}
show(fn)

// 输出10
var fn = function() {
    console.log(fn)
}
fn() // function() { console.log(fn) }

var obj = {
    fn2: function() {
        console.log(fn2) // 没有fn2这个变量
        console.log(this.fn2)
    }
}
obj.fn2() // 报错 fn2 is not defined

闭包

<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
var btns = document.getElementsByTagName('button')
// 遍历加监听
// for(var i = 0; i < btns.length; i++) { // btns.length 每次需要计算才能得到,计算多遍
/* for(var i = 0, length = btns.length; i < length; i++) { // 长度只计算一次
  var btn = btns[i]
  btn.onclick = function() {
    alert('第' + (i + 1) + '个')
  }
} */
// 利用闭包
for(var i = 0, length = btns.length; i < length; i++) { // 长度只计算一次
  (function(i) {
    var btn = btns[i]
    btn.onclick = function() {
      alert('第' + (i + 1) + '个')
    }
  })
} 

1、如何产生闭包?

当一个嵌套的内部(子)函数引用了外部(父)函数的变量(函数)时,就产生了闭包

2、闭包到底是什么?

使用Chrome调试查看:
理解一:闭包是嵌套的内部函数(大部分人)
理解二:包含被引用变量(函数)的对象(极少数人)

注意:闭包存在于嵌套的内部函数中

3、产生闭包的条件

函数嵌套、内部函数引用了外部函数的数据(变量/函数)

function fn1() {
    var a = 2
    var b = '123'
    function fn2() { // 执行函数定义就会产生闭包(不用调用内部函数)
        console.log(a)
    }
}
fn1()

4、常见的闭包

  • 将函数作为另一个函数的返回值
function fn1() {
    // 此时闭包就已经产生了(函数提升,内部函数对象已经创建了)
    var a = 2
    function fn2() {
        a++
        console.log(a)
    }
    return fn2
}   
var f = fn1()
f() // 3
f() // 4
f = null // 闭包死亡(包含闭包的函数对象成为垃圾对象)
  • 将函数作为实参传递给另一个函数调用
function showDelay(msg, time) {
    setTimeout(function() {
        alert(msg)
    },time)
}
showDelay('lalaa', 2000)

5、闭包的作用

问题:

  1. 函数执行完后,函数内部声明的局部变量是否还存在?--一般不存在,存在于闭包中的变量才可能存在
  2. 在函数外部能直接访问函数内部的局部变量吗?-- 不能,但我们可以通过闭包让外部操作它
  • 使用函数内部的变量在函数执行完后,仍然存活在内存中(延长了局部变量的生命周期)
  • 让函数外部可以操作(读写)到函数内部的数据(变量/函数)

6、闭包的生命周期

产生:在嵌套函数内部函数定义执行完时就产生了(不是在调用)
死亡:在嵌套的内部函数成为垃圾对象时

7、闭包的应用

  • 定义JS模块
    • 具有特定功能的js文件
    • 将所有的数据和功能都封装在一个函数内部(私有的)
    • 只向外暴露一个包含n个方法的对象或函数
    • 模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能
function myModule() {
    // 私有变量
    var msg = 'study'
    // 操作数据的函数
    function doSomething() {
        console.log('doSomething()' + msg.toUpperCase())
    }
    function doOtherthing() {
        console.log('doOtherthing()' + msg.toUpperCase())
    }
    
    // 向外暴露对象(给外部使用的方法)
    return {
        doSomething: doSomething,
        doOtherthing: doOtherthing
    }
}

使用:
var module = myModule()
module.doSomething()
module.doOtherthing()

// 匿名函数
(function() {
    // 私有变量
    var msg = 'study'
    // 操作数据的函数
    function doSomething() {
        console.log('doSomething()' + msg.toUpperCase())
    }
    function doOtherthing() {
        console.log('doOtherthing()' + msg.toUpperCase())
    }
    
    // 向外暴露对象(给外部使用的方法)
    window.myModule2 = {
        doSomething: doSomething,
        doOtherthing: doOtherthing
    }
})()

使用:
window.doSomething()
window.doOtherthing()

8、缺点

函数执行完后,函数内的局部变量没有释放,占用内存时间会变长
容易造成内存泄露

解决

  • 能不用闭包就不用
  • 及时释放

内存溢出和内存泄露

内存溢出

  • 一种程序运行时出现的错误,当程序运行需要的内存超过了剩余的内存时,就抛出内存溢出的错误 内存泄露
  • 占用的内存没有及时释放,内存泄露积累多了就容易导致内存溢出 常见的内存泄露:意外的全局变量、没有及时清理的计时器或回调函数、闭包
var name = 'the window'
var object = {
  name: 'my object',
  getNameFunc: function() {
    return function() {
      return this.name
    }
  }
}
alert(object.getNameFunc()()) // the window


var name2 = 'the window'
var object2 = {
  name2: 'my object',
  getNameFunc: function() {
    var that = globalThis
    return function() {
      return that.name2
    }
  }
}
alert(object2.getNameFunc()()) // my object
function fun(n, o) {
  console.log(o)
  return {
    fun: function(m) {
      return fun(m, n) // 外层的
    }
  }
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);
// undefined 0 0 0
var b = fun(0).fun(1).fun(2).fun(3)
// undefined 0 1 2
var c = fun(0).fun(1); c.fun(2); c.fun(3)
// undefined 0 1 1

有闭包,由参数n导致----有没有产生新的闭包

对象创建模式

方式一:Object构造函数模式
套路:先创建空Object对象,再动态添加属性/方法 适用场景:起始时不确定对象内部数据
问题:语句太多

var p = new Object()
p.name = 'Tom'
p.age = 23
p.setName = function (name) {
    this.name = name
}

方式二:对象字面量模式
套路:使用{}创建对象,同时指定属性/方法
适用场景:起始时对象内部数据是确定的
问题:如果创建多个对象,有重复代码

var p = {
    name: 'Tom'age: 23,
    setName: function (name) {
        this.name = name
    }
}

方式三:工厂模式
套路:通过工厂函数动态创建对象并返回
适用场景:需要创建多个对象
问题:对象没有一个具体的类型,都是Object类型

// 返回一个对象的函数==>工厂函数
function createPerson(name, age) {
    var obj = {
        name: name,
        age: age,
        setName: function (name) {
            this.name = name
        }
    }
    return obj
}

方式四:自定义构造函数模式
套路:自定义构造函数,通过new创建对象
适用场景:需要创建多个类型确定的对象
问题:每个对象都有相同的数据,浪费内存

// 定义类型
function Person(name, age) {
    this.name = name
    this.age = age
    this.setName = function (name) {
        this.name = name
    }
}

var p1 = new Person('Tom', 12)
p1.setName = 'Jack'

方式六:构造函数+原型的组合模式
套路:自定义构造函数,属性在函数中初始化,方法添加到原型上
适用场景:需要创建多个类型确定的对象

function Person(name, age) {
    this.name = name
    this.age = age
    this.setName = function (name) {
        this.name = name
    }
}
Person.prototype.setName = function (name) {
    this.name = name
}

继承模式

原型链继承

套路:

  1. 定义父类型构造函数
  2. 给父类型的原型添加方法
  3. 定义子类型的构造函数
  4. 创建父类型的对象赋值给子类型的原型
  5. 将子类型原型的构造属性设置为子类型
  6. 给子类型原型添加方法
  7. 创建子类型的对象:可以调用父类型的方法

关键:
子类型的原型为父类型的一个实例对象

// 父类型
function Supper() {
  this.supProp = 'Supper prototype'
}

Supper.prototype.showSupperProp = function() {
  console.log(this.supProp)
}

// 子类型
function Sub() {
  this.supProp = 'Sub prototype'
}

// 子类型的原型为父类型的一个实例对象
Sub.prototype = new Supper() // 原型链继承
// 让子类型的原型的constructor指向子类型
Sub.prototype.constructor = Sub
Sub.prototype.showSubProp = function() {
  console.log(this.supProp)
}

var sub = new Sub()
sub.showSupperProp()
console.log(sub.constructor) // 没有 Sub.prototype.constructor = Sub 输出结果为 Supper

image.png

借用构造函数继承(假的 )

套路:

  1. 定义父类型构造函数
  2. 定义子类型构造函数
  3. 在子类型构造函数中调用父类型构造函数

关键:
在子类型构造函数在使用super()调用父类型构造函数

function Person(name,age) {
    this.name = name
    this.age = age
}
function Student(name, age, price) {
    Person.call(this, name, age) // 相当于:this.Person(name, age)
    this.price = price
}

var s = new Student('Tom', 20, 10000)
console.log(s.name, s.age, s.price)

组合继承

  1. 利用原型链实现对父类型对象的方法继承
  2. 利用super()借用父类型构造函数初始化相同属性
function Person(name,age) {
    this.name = name
    this.age = age
}
Person.prototype.setName = function (name) {
    this.name = name
}
function Student(name, age, price) {
    Person.call(this, name, age) // 为了得到属性
    this.price = price
}
Student.prototype = new Person() // 为了能看到父类型的方法
Student.prototype.constructor = Student // 修正constructor属性
Student.prototype.setPrice = function (price) {
    this.price = price
}

var s = new Student('Tom', 20, 10000)
s.setName('Bob')
s.setPrice(4000)
console.log(s.name, s.age, s.price)

进程与线程

进程:程序的一次执行,它占有一片独立的内存空间;可以通过Windows任务管理器查看进程
线程:是进程内一个独立执行单元,是程序执行的一个完整流程,是CPU的最小调度单元
关系:一个进程至少有一个线程(主),程序是在某个进程中的某个线程执行的

image.png

浏览器内核模块组成

主线程

  • js引擎模块:负责js程序的编译与运行
  • html/css文档解析模块:负责页面文本的解析
  • DOM/CSS模块:负责dom/css在内存中的相关处理
  • 布局和渲染模块:负责页面的布局和效果的绘制(内存中的对象)

分线程

  • 定时器模块:负责定时器的管理
  • 事件响应模块:负责事件的管理
  • 网络请求模块:负责ajax请求

js线程

1、js是单线程执行的(回调函数也是在主线程)
2、H5提出了实现多线程的方案:Web Workers ----> Worker 3、只能是主线程更新界面

定时器问题

1、定时器并不能保证真正定时执行
2、 一般会延迟一丁点(可以接受),也有可能延迟很长时间(不能接受)

事件循环(轮询)模型

1、代码分类
1)初始化执行代码(同步代码)--包含绑定dom事件监听、设置定时器、发送ajax请求的代码
2)回调执行代码(异步代码)--处理回调逻辑
2、js引擎执行代码的基本流程 初始化代码===>回调代码
3、模型的2个重要组成部分
1)事件(定时器/DOM事件/Ajax请求)管理模块
2)回调队列
4、模型的运转流程
1)执行初始化代码,将事件回调函数交给对应模块管理
2)当事件发生时,管理模块会将回调函数及其数据添加到回调队列中
3)只有当初始化代码执行完后(可能需要一段时间),才会遍历读取回调队列中的回调函数执行
4、模型原理图 image.png

web workers

1、H5规范提供了js分线程的实现,取名为:Web Workers
2、相关API
Worker:构造函数,加载分线程执行的js文件
Worker.prototype.onmessage:用于接收另一个线程的回调函数
Worker.prototype.postMessage:向另一个线程发送消息
3、不足
worker内不能操作DOM(更新UI)
不能跨域加载JS
不是每个浏览器都支持这个新特性

var input = document.getElementById('number')
document.getElementById('btn').onclick = function() {
    var number = input.value
    
    // 创建一个Worker对象
    var worker = new Worker('worker.js')
    // 绑定接收消息的监听
    worker.onmessage = function(event) {
        console.log('主线程向分线程返回的数据' + event.data)
        alert((event.data))
    }
    // 向分线程发送消息
    worker.postMessage(number)
    console.log('主线程接收分线程返回的数据' + number)
}

分线程

function fibonacci(n) {
    // f(n) = f(n-1) + f(n-2)
    return n <= 2 ? 1 : fibonacci(n - 1) + fibonacci(n - 2) // 递归调用
}
var onmessage = function(event) { // 不能使用函数声明
    var number = event.data
    console.log('分线程接收到主线程发送的数据' + number)
    // 计算
    var result = fibonacci(number)
    postMessage(result)
    console.log('分线程向主线程返回数据' + result)
}