jsAdvance
数据类型
说明
数据类型指的是变量的内存值的类型;
基本类型指变量存储的就是基本类型本身的数据;
引用类型指的是变量存储的是类型数据的地址;
a = b 指的是把a复制一份给b
分类
基本类型
- number
- boolean
- string
- undefined
- null
- Symbol
对象类型(引用)
- Object: 任意对象(一般对象封装数据,键值对形式)
- Function:特殊的对象(封装执行代码)
- Array:特殊对象(数值下标,内部数据有序)
判断
typeof const
(返回数据类型的字符串表达)
不能区别null 和 object
object 和 array
都返回objecta instanceof B
a 是否为B 的实例 (返回Boolean)
如果B函数的显式原型对象在a的原型链上,则返回true
=== / ==
(后者会类型转换)
相关问题
- undefined 和 null 的区别?
答:undefined定义了,但是没有赋值;null定义且赋值,值为null - 为何需要赋值为null?
答:初始赋值为null,表明将要赋值为引用类型;结束赋值为null,变成垃圾对象 - js调用函数时传递的变量参数是值传递还是引用传递?
答:值传递。会把实参的内容复制一份给形参,形参只在函数作用域中有效,即使实参与形参同名,但是形参也是一个新的变量,且作用域在函数内部,改变后也不影响外部的实参(除非return) - 内存的生命周期:
分配空间,得到使用权;
存储数据,可以反复进行操作;
释放空间 - 释放内存
全局变量:浏览器结束后自动释放
局部变量:函数执行完毕后自动释放
对象:没有被调用的对象会成为垃圾对象 ⇒ 垃圾回收器回收
栈与堆
栈中存储全局变量和局部变量;堆中存储对象
引用赋值问题:
let a = {name: 'zs'}
let b = a;
a.name = 'ls';
console.log(b.name) //结果为ls
let c = {age: 13}
let d = c;
c = {age:18, name: 'xw'} //等于开辟了新的对象空间
console.log(d.name, d.age) //结果为undefined和13
函数引用赋值问题:
let e = {age: 18}
function fn1(obj){
obj = {age:20}
}
function fn2(obj){
obj.age = 21;
}
function fn3 (obj){
obj = {age:22}
return obj
}
console.log(e.age); //18
fn1(e);
console.log(e.age); //18
fn2(e);
console.log(e.age); //21
e = fn3(e);
console.log(e.age); //22
对象
定义
多个数据的封装体,代表真实世界的一个事物
组成
属性:属性名(字符串类型) + 属性值(任意类型)
方法: 一种特殊的属性
调用方式
-
obj.属性名
属性名包含特殊字符(-,空格等)
属性名不确定
以上情况都不能用.的方式 -
obj['属性名'] 或 obj[变量属性名]
任意情况通用
函数
定义函数
- 函数声明
function fn1 (){
console.log("fn1")
}
- 表达式
let fn2 = function (){
console.log("fn1")
}
函数的调用
test()
直接调用obj.test()
对象调用new Test()
构造函数调用test.call / apply(obj)
改变this指向并立刻执行函数(临时的让test变成obj的方法进行调用)
回调函数
定义
- 自己定义
- 自己没有调用
- 最终执行了
分类
dom
事件回调函数setTimeout(fn, time) getTimeout(fn,time)
定时器回调函数Ajax
请求回调函数promise
回调函数- 生命周期函数
IIFE(immediate-invoked function expression) – 匿名函数自调用
// ;(函数内容)()
// 加上分号预防编译时括号与上层连接报错
;(function (){
let a = 30
let outFn = () => {
console.log(++a)
}
let inFn = () => {
console.log("never use")
}
//用window定义的变量会升级成全局变量
window.$ = () => {
return {outFn: outFn}
}
})()
作用
- 实现隐藏
- 不会污染外部(全局)命名空间
- 编码js模块化
函数中的this
什么是this
前提:任何函数本质上都是通过对象去进行调用的,如果没有对象,那么指定的就是window
- 所有函数内部都有一个
this
变量 this
的值指的是调用函数的对象(谁调用this指谁)- 箭头函数没有
this
判断this
test()
windownew Person()
windowp.test()
pvar p = new Person()
新创建的对象( p )p.test.call(obj)
obj
函数中的prototype属性
- 每个函数都有一个
prototype
(显式)属性,默认指向一个Object
空对象(即:原型对象),Object
的原型对象例外 - 每个对象都有一个
constructor
属性,它指向函数对象
即:Person.prototype.constructor === Person
- 给原型链添加属性和方法(一般为方法),则其所有的实例对象都可以调用这些属性与方法
- 读取
prototype
中的值时,直接用person1.type
- 先在自己身上找,找到即返回
- 自己身上找不到,则沿着原型链向上查找,找到即返回
- 如果一直到原型链的末端还没有找到,则返回
undefined
- 每个实例对象都有一个
__proto__
(隐式)属性,它的值也是prototype所指向的那个对象,ES6以后可以调用隐式属性,但是不推荐使用,可以实例对象直接调用属性与方法
console.log(Person.prototype)
console.log(xiaoming.__proto__)
console.log(Person.prototype === xiaoming.__proto__) //true
内存浪费问题.
对于每一个实例对象,type
属性 和sayHi
方法 都是一模一样的内容,
每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。
- JavaScript 规定,每一个函数都有一个
prototype
属性,指向另一个对象。
这个对象的所有属性和方法,都会被构造函数和实例所拥有。
function Person(name){
this.name = name
this.sayHi = function (){
console.log('hi')
}
}
Person.prototype.showName = function (){
console.log(`your name is ${this.name}`)
}
let xiaoming = new Person('ming')
xiaoming.sayHi()
xiaoming.showName()
xiaoming.toString()
原型链
别名: 隐式原型链
作用: 查找 对象 属性
链结构(一般): 实例对象 > 构造函数的prototype(Object的实例)
> Object.prototype
> null
要点:
- 实例对象都有
__proto__
等于函数prototype
指向的对象(对象创建时js引擎自动添加) - 任何的函数都会有一个
prototype
指向一个实例对象(在函数创建后js引擎自动new
出来)
变量与函数提升
变量只会提升(var定义的)变量名,函数会直接提升函数名和内容
console.log(a1) //undefined
a2() //a2()
a3() //undefined
console.log(a4) //error(如果是纯ES6时会报错;但是如果全局有var的使用,就会直接忽略语句(用source调试是undefined且没有绑定到window上),不执行也不报错)
var a1 = 'a1'
let a4 = 'a4'
function a2() {
console.log('a2()')
}
var a3 = ()=>{
console.log("a3")
}
变量提升原理(执行上下文机制)
执行上下文
- 全局执行上下文
在js运行开始时会创建一个对象,名为window并且将window确定为全局执行上下文
- 对全局数据进行预处理:
- 把var定义的全局变量 ==>undefined,添加为window属性
- 把function *** 声明的函数 ==>并赋值(fn),添加为window方法
- this ===>赋值(window)
- 开始由上而下执行js代码
- js执行完毕,回收window对象
- 局部执行上下文
在调用函数,准备执行函数之前,创建对应函数执行的局部(函数)上下文
- 对局部数据进行预处理
- 形参变量 ==>赋值(实参), 添加为局部上下文的属性
arguments
==> 赋值(实参列表), 变为伪数组, 添加为局部上下文属性- 提升var的变量 ⇒ undefined, 添加为局部上下文属性
- 提升声明函数 ==> 赋值内容 ,添加为局部上下文的方法
- this 赋值 (调用函数的对象)
- 开始执行函数代码
- 函数执行完成,回收(清除)局部上下文空间对象
执行上下文栈
var a =10
var bar = function (x){
var b =5
foo(x + b)
}
var foo = function(y){ //变量函数,会变量提升(虽然声明在bar之后,但是在执行前,所以函数可以执行)
var c = 5
console.log(a + c +y)
}
bar(10)
在全局代码开始执行前,JS引擎会创建一个栈来存储管理所有的执行上下文对象
在window
确定后,会将其加入栈中(压栈)
在函数准备执行时 (局部执行上下文创建后) 会将其压入栈中
在函数执行完毕后会把局部执行上下文进行出栈操作
所有代码执行完毕后,栈中只剩下window
作用域
作用域是静态的, 只要定义了,就确定好了,与执行过程无关
作用
隔离变量,不同的作用域下同名变量不会冲突
分类
作用域只有 全局作用域 和 函数作用域 之分
- 全局作用域
- 函数作用域 (局部)
- 块级作用域 (
{}
中的内容) => ES6以后才有块级作用域
作用域链
次级局部变量 => 主级局部变量 => 全局变量
(由内而外查找)
面试题
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() //fn函数
var Obj = {
fn2: function () {
console.log(fn2) //函数作用域没有,全局作用域也没有
},
fn3:function (){
console.log(this.fn3) //任何属性的调用都需要加上其 (对象。)
}
}
Obj.fn2() //报错
Obj.fn3() //fn3函数
闭包
产生
当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(函数)时,就产生了闭包
个人理解: 内部函数在调用了外部函数的变量后,会把调用了的外部函数变量保存到自身的闭包,且一直存在,不随函数(父函数或子函数)的执行上下文结束而结束。父函数中的同一个变量被不同子函数引用,是对同一个变量进行操作(可以理解成在堆中开辟了块父函数的闭包空间,且调用的变量作为属性永久存储在此。任意子函数的闭包数据都是从这个堆空间中取出,所以操作数据相同)
function fn(){
var a = 'closure'
var b = 'none'
function f1(){
//理解一:闭包对象(嵌套函数包含了一个对象,对象属性就是引用了的外部属性,如 a)
//理解二:闭包就是嵌套的内部函数
console.log(a)
}
}
作用
- 将内部函数作为外部函数的返回值返回
- 将函数(一个子函数)作为实参传递给另一个子函数
- 定义js模块
- 具有特定功能的js文件
- 将所有的数据和功能都封装在一个函数内部 (私有)
- 只向外暴露一个包含n个方法的对象或函数
- 模块的使用者,只需要通过模块暴露的对象调用方法来实习对应的功能
目的
- 使用函数内部的变量在函数执行完后,仍然存活在内存中 (延长了局部变量的生命周期)
- 让函数外部可以操作(读/写)到函数内部的数据(变量/函数)
function fn(){
var a = 0
function fn2(){
a++
console.log(a)
}
return fn2
}
var closure = fn()
closure() //1
closure() //2
闭包的生命周期
产生:
在函数定义执行完就产生了闭包 (执行上下文)
死亡:
在嵌套的内部函数(闭包函数) 成为了垃圾对象。
※注意: 是内部函数对象自身成为了垃圾对象 (没人引用), 而不是说指向此函数的变量。因为变量自身不在闭包中,所以在父函数执行完就会死亡。如上文的 fn2 是一定会死亡的,但是他指向的函数(引用类型是地址值)被全局 closure
接收了,闭包就一直存在了
closure = null
闭包没人引用,成为垃圾对象,生命周期结束
缺点及解决
- 局部变量没有释放, 占用内存时间长
- 容易造成内存泄漏
解决
- 能不用闭包就不用
- 及时释放
var f = fn()
f()
f = null //没人引用,闭包函数变成垃圾对象
内存
溢出
- 程序运行时出现的错误
- 当程序运行需要的内存超过了 剩余的内存时就会出现溢出错误
泄露
- 程序可以正常运行,只是泄露了内存,可用空间减少
- 泄露过多可能会导致溢出
- 占用的内存没有释放
- 意外的全局变量
a = 123
(没加var) - 没有及时清理的计时器和回调函数
- 闭包
- 意外的全局变量
垃圾回收
只要数据没有被任何变量引用,就会被进行垃圾回收
var o = {
a: {
b:2
}
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集
var o2 = o; // o2变量是第二个对“这个对象”的引用
o = 1; // 现在,“这个对象”只有一个o2变量的引用了,“这个对象”的原始引用o已经没有
var oa = o2.a; // 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa
o2 = "yo"; // 虽然最初的对象现在已经是零引用了,可以被垃圾回收了
// 但是它的属性a的对象还在被oa引用,所以还不能回收
oa = null; // a属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
/*
{
a: {
b:2
}
}
*/
对象
构造方法
new Object()
动态添加属性- 字面量模式
{name: 'ls', age: 18}
- 工厂函数
- 构造函数 (函数名记得大写)
继承
子类的原型变成父类型的实例对象
Son.prototype = new Father()
这种方法可以使子的原型是父,父的原型才是ObjectSon.prototype.constructor = Son
加上构造器,使原型数据更准确
通过call方法 (假继承)
function Person(name,age){
this.name = name
this.age = age
}
function Student(name,age, sources){
Person.call(this,name, age)
//这里的this指的是new Student时的实例
//call 就是构造函数假继承,为了获得属性
this.sources = sources
}
const stu1 = new Student('zs', 18, [100,99,99])
console.log(stu1)
组合方式 (两种结合)
function Person(name,age){
this.name = name
this.age = age
}
Person.prototype.setname = function(name){
this.name = name
}
function Student(name, age, sources){
Person.call(this,name, age) //call 构造函数假继承
this.sources = sources
}
Student.prototype = new Person() //父类实例继承
Student.prototype.constructor = Student
Student.prototype.setsources = function(sources){
this.sources = sources
}
const stu1 = new Student('zs', 18, [100,99,99])
stu1.setname('ll')
stu1.setsources([99,150,120])
console.log(stu1)
进程与线程
定义
进程: 程序的一次执行, 他占有一片独有的内存空间
可以通过 windows 任务管理器查看进程
线程:
- 进程内的一个独立执行单元
- 是程序执行的一个完整流程
- 是cpu的最小调度单元
一个程序可以有多进程,一个进程可以有多线程
相关知识
- 应用程序必须运行在某个进程的某个线程上
- 一个程序至少有一个运行的线程:主线程 (进程启动后自动创立)
- 一个进程可以多线程工作
- 一个进程内的数据可以供其中多个线程直接共享
- 多个进程间的数据是各自独立的
- 线程池:保存多个线程对象的容器,实现线程对象的反复利用
- js是单线程运行
- H5 中使用 Web Workers 可以启用多线程 (主-分线程)
- 浏览器是多线程的 : 网络线程、渲染线程等
- 新版浏览器多进程
浏览器内核
支撑浏览器运行的最核心的程序
一个标签窗口就会开创一个新的进程
模块分类
主线程
- js引擎模块: 复制编译、运行js
- html, css 文档解析模块: 页面文本的解析
- DOM / CSS模块: 负责dom, css在内存中的相关处理
- 布局,渲染模块: 页面的布局和效果的绘制 (内存中的对象)
分线程
- 定时器模块
- 事件响应模块
- 网络请求模块
定时器
问题
定时器并不能保证准确按照延迟时间执行
一般会延迟一点, 也可能会长时间延迟 (异步)
原因
定时器的回调函数 (或者说任何js函数) 都是在主线程执行的,因为js是单线程的. 只是定时器模块(计时过程)是在分线程执行
js单线程
alert()
旧版会暂停主线程以及阻塞计时器; 新版不再会阻塞计时器, 会暂停主线程
js引擎流程
- 先执行完所有的初始化代码 (同步代码),
包括- 设置定时器
- 绑定dom监听和事件监听
- 发送Ajax请求
- 后面的某个时间才会执行回调代码 (异步代码)
事件循环模型
- 所有代码均在执行栈中执行 (stack)
- 回调队列中的回调代码是通过事件循环机制一个接一个的传进执行栈中执行
- 执行栈, 浏览器分线程, 回调队列 三者形成一个循环圈, 不停更新直到窗口关闭
事件循环
每个"线程"都有自己的事件循环, 每个标签窗口对应一个事件循环
事件循环持续运行,执行排队执行的任何任务。事件循环具有多个任务源,可保证该源内的执行顺序.
但浏览器可以选择从循环的每个回合中获取任务的来源。这允许浏览器优先执行敏感任务(如用户输入)
在任务之间,浏览器可能会呈现更新。从鼠标单击到事件回调需要安排任务,解析 HTML 也是如此
JS引擎 (任务队列 JS stack)
JS为单线程执行,所有任务只有进入js队列中按顺序执行
所有的定义,赋值,函数执行 (执行上下文)都属于简单任务队列
微任务队列 (Microtasks)
微任务通常被安排在当前执行脚本之后应该立即发生的事情. 在微任务期间排队的任何其他微任务都添加到队列的末尾并进行处理。
Pomise请求属于微任务队列, 通过回调函数返回的对象继续 .***() 的方法
比如Pomise执行完回调后 .then()
就是微任务
宏任务队列 (回调队列 callback queue)
所有的异步操作,所有的回调函数都会进入回调队列中. 等待处理
所有的请求,事件,定时器都属于回调队列
事件循环过程
- JS引擎由上而下开始执行代码,把所有代码分类进入不同的队列 (执行上下文算一个宏任务,且第一个放入JS引擎中执行)
- 从队列取出操作的数量应该按照cpu的刷新时间来决定
- 先处理任务队列中的所有任务,
- 当任务队列清空后,会进入微任务队列中,把微任务队列中的任务放入JS任务队列, 处理完成把 .then(()=>{}) 放入微任务队列, 再从微任务队列中拿出第一个放入JS任务队列,以此循环,直至微任务队列没有操作
- 当任务队列和微任务队列中都没有了任何操作后, 会从回调队列中拿出需要执行的回调放入JS任务队列,执行清空JS任务队列
以此循环,只要窗口不关闭,任务循环就会一直执行
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(function () {
console.log('promise1');
})
.then(function () {
console.log('promise2');
});
console.log('script end');
结果为: script start => script end => promise1 => promise2 => setTimeout
某些浏览器会把promise
作为回调队列, 结果为 setTimeout => promise1