JavaScript学习笔记(三)

JavaScript学习笔记(三)

前言: 本篇笔记主要知识点:原型与原型链 +上下文与作用域+ 闭包 +继承模式。

一.基础总结

1.数据类型

分类
  1. 基本(值)类型

    string,number,boolean,undefined,null + SymbolBigint(ES6新增)

    undefined是定义了但是未赋值,null是定义且赋值,但值为null而已。

  2. 对象(引用)类型

    • Object,Function,Array
关于判断
  1. typeof :返回的是数据类型的字符串表达(全小写)

可以判断 undefined+number+string+boolean+function

不可判断 null,object 与 array

typeof null == object;
typeof [] == object;
  1. instanceof:可以判断具体类型
  2. === : 可以判断undefined,null

2.数据,变量与内存

1.什么是数据?

  • 存储在内存中代表特定信息的’东东’,本质上是0101…

2.什么是内存?

  • 内存条通电后产生的可储存数据的空间(临时的)
  • 内存产生和死亡:内存条(电路版)>通电>产生内存空间==>存储数据==>处理数据==>断电==>内存空间和数据都消失
  • 一块小内存的2个数据

内存分类:栈:全局变量/局部变量 堆:对象

3.什么是变量?
*可变化的量, 由变量名和变量值组成
*每个变量都对应的一块小内存变量名用来查找对应的内存,变量值就是内存中保存的数据
4.内存,数据,变量三者之间的关系.

内存用来存储数据,变量是内存地址的标识

3.对象

  • 什么是对象? 找不到的女朋友
    *代表现实中的某个事物,是该事物在编程中的抽象
    *多个数据的集合体(封装体)
    *用于保存多个数据的容器

  • 为什么要用对象?
    *便于对多个数据进行统一管理

  • 对象的组成:属性+方法。

    属性

    • 代表现实事物的状态数据
    • 由属性名和属性值组成
    • 属性名都是字符串类型,属性值是任意类型

    方法

    • 代表现实事物的行为数据
    • 是特别的属性==>属性值是函数
  1. 如何访问对象内部数据?
    .属性名 : 编码简单,但有时不能用
    [属性名] : 编码麻烦,但通用

4.函数

为什么要用函数?

  • 提高代码复用
  • 便于阅读交流

如何定义函数?

  • 函数声明
  • 表达式

如何调用(执行)函数?

  • test():直接调用
  • obj. test():通过对象调用
  • new test(): new调用
  • test. call/apply(obj):临时让test 成为bj的方法进行调用

常见的回调函数

  • dom事件回调函数
  • 定时器回调函数
  • ajax请求回调函数
  • 生命周期回调函数
匿名函数自调用 “IIFE"

immediately-invoked Function Experssion

(function (){//匿名函数自调用
 	   ...
})()

作用:1. 隐藏实现 2. 不会污染外部命名空间3. 编写JS模块

函数中的this
function Say(){
	console.log(this);
    this.get(){
        console.log(this);
    }
}
Say();  //window
var p = new Say(); //p
p.get(); //p
var obj = {};
p.get.call(obj);//obj
var test = p.get;
test();//window

5.关于语句分号

1.js-条语句的后面可以不加分号

2.是否加分号是编码风格问题, 没有应该不应该,只有你自己喜欢不喜欢

3.在下面2种情况下不加分号会有问题

  • 小括号开头的前一条语句
  • 中方括号开头的前一条语句

4.解决办法:在行首加分号
JS合并的时候有可能会出现问题,开头加个;就Ok了
比如这样的行首代码,可以防止JS合并文件时候的语句分号问题

;(function(){
    ...
})()

二.函数高级

1. 原型与原型链

小结: 对象有__proto__属性,函数对象有prototype属性,原型对象有constructor属性。

原型

在JavaScript中,原型也是一个对象,通过原型可以实现对象的属性继承,JavaScript的对象中都包含了一个”[[Prototype]]”内部属性,这个属性所对应的就是该对象的原型。

1.函数的prototype属性

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

Date.prototype.constructor === Date
func.prototype.constructor === func

(PS:Object 、Function 是 JS 自带的函数对象。)

显示原型与隐式原型

1.每个函数function 都有一个prototype, 即显式原型
2.每个实例对象都有一个_ proto__, 可称为隐式原型

3.对象的隐式原型的值为其对应构造函数的显式原型的值
4.总结:

  • 函数的prototype属性: 在定义函数时自动添加的,默认值是一个空0bject对象
  • 对象的__proto__ 属性:创建对象时自动添加的,默认值为构造函数的prototype属性值
  • 程序员能直接操作显式原型但不能直接操作隐式原型(ES6之前)
function Fn(){} //构造函数
var fn = new Fn(); //内部语句:this.__proto__ = Fn.prototype
console.log(fn.__proto__)
Fn.prototype === fn.__proto__  //true
function Person() {}
var p = new Person();
//方法才有prototype,普通对象无prototype
console.log(Person.prototype); // Object{} 
console.log(p.prototype); // undifined

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

Fn.prototype.test = fucntion(){
	console.log('test');
}

实例可以修改自身的数据类型的属性,但是不能修改原型的属性

如果修改的是引用值,那么可以修改原型的。

在这里对“prototype”和“__proto__”进行简单的介绍:
对于所有的对象,都有__proto__属性,这个属性对应该对象的原型;

对于函数对象,除了__proto__属性之外,还有prototype属性,当一个函数被用作构造函数来创建实例时,该函数的prototype属性值将被作为原型赋值给所有对象实例(也就是设置实例的__proto__属性)

原型链

因为每个对象和原型都有原型,对象的原型指向原型对象,
而父的原型又指向父的父,这种原型层层连接起来的就构成了原型链。

1.原型链

  • 访问一个对象的属性时,先在自身属性中查找,找到返回;如果没有,再沿着__proto__ 这条链向上查找,找到返回,如果最終没找到,返回undefined
  • 别名:隐式原型链
  • 作用:查找对象的属性(方法)

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

关系图

上面实例对象person的内部指针就是__proto__.

3.原型链
每个实例对象( object)都有一个私有属性 (称之为__proto__指向它的构造函数的原型对象(prototype)。该原型对象也有一
个自己的原型对象(__proto__ ) ,层层向上直到一个对象的原型对象为null。根据定义,null没有原型,并作为这个原型链中的
最后一个环节。(其实最后一个对象一般就是Object…)

知识点:

1.函数的显示原型指向的对象默认是空0bject实例对象(但object不满足);

2.所有函数都是Function的实例(包含Function);

3.读取对象的属性值时:自动到原型链中查我;

4.设置对象的属性值时:不会查找原型链,如果当前对象中没有此属性,直接添加此属性并设置其值;

5.方法一般定义在原型中,属性一般通过构造函数定义在对象本身上.

instanceof 原理
A instanceof B

如果B函数的显式原型对象在A对象的原型链上,返日true, 否则返回false。

console.log(Object instanceof Function) // true
console.log(Object instanceof Object) // true
console.log(Function instanceof Function) // true
console.log(Function instanceof Object) // true

2. 执行上下文与执行上下文栈

变量提升与函数提升

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

执行上下文

1.代码分类(位置)
*全局代码
*函数(局部代码
2.全局执行上下文
*在执行全局代码前将window确定为全局执行上下文
*对全局数据进行预处理

  • var定义的全局变量==>undefined,添加为window的属性
  • function声明的全局函数==>赋值(fun),添加为window的方法
  • this==>赋值(window)
    开始执行全局代码

3.函数执行上下文
*在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象
*对局部数据进行预处理
*形参变量==>赋值(实参)==>添加为执行上下文的属性

*arguments==> 赋值(实参列表),添加为执行上下文的属性
*var定义的局部变量==>undefined,添加为执行上下文的属性
*function声明的函数==>赋值(fun),添加为执行上下文的方法
*this==>赋值(调用函数的对象)
*开始执行函数体代码

执行上下文栈

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

面试题
var a = 3
function fn(){  //函数提升
    console.log(a)
    var a = 4   //点:变量提升
}
fn()//输出 undefined
fn3() //输出undefined , fn3视作变量
var fn3 = function(){
    //...
}
//测试1
function a(){}
var a ;
console.log(typeof a)
//先执行变量提升,再执行函数提升
//输出 'function'

//测试2
if(!(b in window)){
    var b = 1;
}
console.log(b)
// undefined 
//测试3
var c = 1 
function c(c){
    console.log(c)
}
c(2)  // 报错
//c is not a function

3. 作用域与作用域链

作用域

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

作用域与执行上下文

1.区别1
*全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时。
*全局执行上下文环境是在全局作用域确定之后,js代码马上执行之前创建。
*函数执行上下文是在调用函数时,函数体代码执行之前创建。

2.区别2
*作用域是静态的,只要函数定义好了就一 一直存在,且不会再变化。
*上下文环境是动态的,调用函数时创建,函数调用结束时. 上下文环境就会被释放。
3.联系
*上下文环境(对象)是从属于所在的作用域
*全局上下文环境==>全局作用域
*函数上下文环境==>对应的函数使用域

面试题
var x=10;
function fn() {
	console.1og(x);
}
function show(f) {
	var x=20;
	f();
}
show(fn);
//输出: 10
var fn = function(){
	console.log(fn)
}
fn() // function
var obj={
	fn2: function () {
		console.log(fn2)
	}
}
obj.fn2() //报错 fn2 is not defined

fn2在函数内找不到,就去全局作用域找了


4. 闭包

你可以在一个函数里面嵌套另外一个函数。嵌套(内部)函数对其容器(外部)函数是私有的。它自身也形成了一个闭包。一个闭包是一个可以自己拥有独立的环境与变量的表达式(通常是函数)。

----摘自"https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Functions"的定义

理解

“闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

函数执行完后,函数内部声明的局部变量是否还存在?

  • 一般是不存在的,存在于闭包中的变量才可能存在

在函数外部可以操作读写到函数内部的数据(变量/函数)

  • 不能,但是我们可以通过闭包让外部操作它。
闭包的生命周期

产生: 在嵌套内部函数定义执行完时就产生了(不是调用)

死亡:在嵌套的内部函数成为垃圾对象时

function fn(){
    //函数提升,内部函数对象已经创建
	var a = 2
    function fn2(){
        a++
        console.log(a)
    }
    return fn2
}
var f = fn()
f() //3
f() //4
f =  null //死亡
作用

闭包的应用2 :定义IS模块
*具有特定功能的js文件
*将所有的数据和功能都封装在一个函数内部( 私有的)
只向外暴露一个包含n个方法的对象或函数
模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能

//module.js
function module(){
    //私有数据
    var msg = '123'  
    //操作数据的行为
    function func1(){
        console.log(msg)
    }
    //通过return 暴露
    return func1
}

//module2.js
function module(){
    var num = 123  
    function add(){
        num++
    }
    function sub(){
        num--
    }
    //通过return 对象暴露
    return {
        add:add,
        sub:sub
    }
}
//上述导入JS文件后再调用函数才能得到暴露对象。


//module3.js
function module(){
    var num = 123  
    function add(){
        num++
    }
    function sub(){
        num--
    }
    //导入JS后,可直接使用Module
    window.Module = {
        add: add,
        sub: sub
    }
}
//extern:通过传入window来方便日后代码压缩。
缺点
  1. 函数执行完后,函数内的局部变量没有试放,占用内存的时间变长。
  2. 容易造成内存泄漏。

解决方法:能不用闭包就不用,及时释放。

相关知识:

1.内存溢出
*一种程序运行出现的错误
*当程序运行需要的内存超过了剩余的内存时,就出抛出内存溢出的错误
2.内存泄露
*占用的内存没有及时释放
*内存泄露积累多了就容易导致内存溢出
常见的内存泄露:
*意外的全局变量
*没有及时清理的计时器或回调函数
*闭包

三.面向对象高级

1.对象创建模式

方式一:Object构造函数模式

套路:先创建空object对象,再动态添加属性1方法
适用场景:起始时不确定对象内部数据
问题:语句太多

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

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

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

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

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)
//再创建p2 , 函数占用的内存浪费

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

//在构造函数中只初始化- -般函数
function Person(name, age) { 
	this.name = name
	this.age = age
}
Person.prototype.setName = function (name) {
	this.name = name
}

2.继承模式

原型链继承

方式1:原型链继承
1.套路
1.定义父类型构造函数
2.给父类型的原型添加方法
3.定义子类型的构造函数
4.创建父类型的对象赋值给子类型的原型
5.将子类型原型的构造属性设置为子类型
6.给子类型原型添加方法

​ 7.创建子类型的对象:可以调用父类型的方法.
2.关键
​ 1.子类型的原型为父类型的一个实例对象

借用构造函数继承

方式2:借用构造幽数继承(假的)
1.套路:
1.定义父类型构造函数
2.定义子类型构造函数
3.在子类型构造函数中调用父类型构造
2.关键:
1.在子类型构造函数中通用call()调用父类型构造函数

function Person(name,age){
    this.name = name
    this.age = age
}
function Student(name,age,price){
    Person.call(this,name,age)
    this.price = price
}

var s = new Student('Tom',20,1400)
console.log(s.name,s.age,s.price)
组合继承

方式3:原型链+借用构造函数的组合继承
1.利用原型链实现对父类型对象的方法继承
2.利用super()借用父类型构建函数初始化相同属性

function Person(name,age){
    this.name = name
    this.age = age
}
function Student(name,age,price){
    Person.call(this,name,age)  // 为了得到属性
    this.price = price
}

Student.prototype = new Person()  //为了看到父类型的方法
Studenet.prototype.constructor = Student//修正constructor属性
Student.prototype.setPrice = function(price){
    this.price = price
}
var s = new Student('Tom',24,15000)
s.setName('Bo')
s.setPrice(1600)
console.log(s.name,s.age,s.price)

3.new 一个对象背后做了什么?

  • 创建一个空对象
  • 给对象设置__proto__,值为构造函数对象的prototype属性值
  • 执行构造函数体(给对象添加属性)

四.线程机制与事件机制

1.进程与线程

进程:程序的一次执行,占用一片独有的内存空间。可以通过windows任务管理器进行查看。

线程:进程内的一个独立运行单元,是程序执行的一个完成流程,CPU调度的最小单位。

应用程序必须运行在某个进程的某个线程上
一个进程中至少有一个运行的线程:主线程,进程启动后自动创建
一一个进程中也可以同时运行多个线程,我们会说程序是多线程运行的
一个进程内的数据可以供其中的多个线程直接共享
多个进程之间的数据是不能直接共享的
线程池(thread pool):保存多个线程对象的容器,实现线程对象的反复利用

--------------相关知识点--------------

多线程

优点:1.能有效提升CPU的利用率

缺点:1.创建多线程开销 2. 线程间切换开销 3.死锁与状态同步问题

单线程

优点:顺序编程,简单易懂

缺点:效率低

浏览器是多线程运行的,有的是多进程(Chrome,新版IE)

2.浏览器内核

支持浏览器运行的最核心的程序

不同的浏览器可能不一样

内核由很多模块组成:JS引擎模块,HTML,CSS文档解析模块,DOM/CSS模块,布局和渲染模块,定时器模块,事件响应模块,网络请求模块(Ajax请求)…

3.定时器引发的思考

1.定时器真是定时执行的吗?
*定时器并不能保证真正定时执行
*-般会延迟一丁点(可以接受),也有可能延迟很长时间(不能接受)
2.定时器回调函数是在分线程执行的吗?
*在主线程执行的, js是单线程的
3.定时器是如何实现的?
*事件循环模型(后面讲)

4.JS是单线程执行的

1.如何证明js执行是单线程的?

*setTimeout() 的回调函数是在主线程执行的
*定时器回调函数只有在运行栈中的代码全部执行完后才有可能执行

//测试代码:
		var btn = document.getElementById('btn')
		btn.onclick = function() {
			setTimeout(function() {
				console.log('2')
			}, 2000)
			setTimeout(function() {
				console.log('3')
			}, 4000)
			alert("1") //弹出警告的时候会暂停上述的计时。(以前)		
		}
//2021.8.12 测试Chrome版本 92.0.4515.131(正式版本) (64 位)
//测试情况:测试时弹出警告框,不点击确定就无法输出‘2’,‘3’,但是(过了四秒)点击确定后,直接输出"2""3"。

2.为什么js 要用单线程模式,而不用多线程模式?

*JavaScript的单线程,与它的用途有关。
*作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。
*这决定了它只能是单线程,否则会带来很复杂的同步问题。

(即便有了H5的多线程语法,也只能有一个线程来操作DOM)

3.代码的分类:
*初始化代码
*回调代码

4.js引擎执行代码的基本流程

*先执行初始化代码:包含一些特别的代码 回调函数(异步执行)
*设置定时器
*绑定监听
*发送ajax请求
*后面在某个时刻才会执行回调代码

5.浏览器的事件循环(轮询)模型

1.所有代码分类
*初始化执行代码(同步代码):包含绑定dom事件监听, 设置定时器, 发送ajax请求的代码
*回调执行代码(异步代码): 处理回调逻辑

2.js引擎执行代码的基本流程:
*初始化代码===>回调代码
3.模型的2个重要组成部分:
*事件管理模块(包括定时器管理模块)
*回调队列
4.模型的运转流程
*执行初始化代码,将事件回调函数交给对应模块管理
*当事件发生时,管理模块会将回调函数及其数据添加到回调列队中
*只有当初始化代码执行完后(可能要- 定时间),才会遍历读钦回调队列中的回调函数执行。

事件循环模型原理图

相关概念

  1. 执行栈:所有的代码都在这里执行
  2. 浏览器内核:JS引擎模块和其他模块。
  3. 任务队列—都属于callback queue
  4. 事件队列—都属于callback queue
  5. 消息队列—都属于callback queue
  6. 事件轮询:从任务队列中循环取出回调函数放入执行栈中处理
  7. 事件驱动模型
  8. 请求响应模型

6.H5 Web Workers(多线程)

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

Web Workers 是HTML5提供的- -个javascript多线程解决方案
我们可以将一-些大计算量的代码交由webWorker运行而不冻结用户界面
但是子线程完全受主线程控制,且不得操作DOM。
所以,这个新标准并没有改变JavaScript单线程的本质

使用:

  1. 创建在分线程执行的JS文件。
  2. 在主线程中的JS中发消息并设置回调。
//创建一个Worker对象并向它传递将在新线程中执行的脚本的URL
var worker = new Worker("worker.js"); 
//接收worker传过来的数据函数
worker.onmessage = function (event) {
	console.log(event.data);
    //event.data是固定的名称,不能变的。
};
//向worker发送数据
worker.postMessage("hello world");

再看看分线程的代码:

//不能用函数声明
var onmessage =function (event){
    console.log('onMessage022');
    var upper = event.data.toUpperCase;//通过event.data获得发送来的数据
    postMessage( upper );//将获取到的数据发送会主线程
}

不足:

  1. 慢(主线程交给分线程,再返回数据给主线程)

  2. 不能跨域加载JS

  3. worker内代码不能访问DOM(UI)

  4. 不是每个浏览器都支持这个新特性

五.参考网址

笔记整理主要来自B站尚硅谷的视频,但是学习中的太多疑惑是从其他渠道来求解的.下面主要是一些学习过程中参考的网址,从中获益良多.

知识点内容
原型与原型链脚本之家的总结
原型与原型链腾讯云博客:从对象的角度来讲解的
原型与原型链掘金:从继承的角度看原型
new一个对象的过程知乎:用士兵案例讲解
语法糖(学习new过程附带的术语)百度百科定义
闭包知乎:简易回答
闭包菜鸟教程:计数器困境的例子
闭包脚本之家:一分钟理解闭包(真会吹,1分钟肯定看不完呐)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

如果皮卡会coding

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值