JavaScript篇
ch1 - 理论
数据类型相关
数据类型分类
JS的数据类型可以分为两大类:基本数据类型、 引用数据类型
- 基本数据类型:Number、 String、 Boolean、 Undefined、 Null、 (symbol、 bigint)
- 引用数据类型:Function、Object、Array
数据类型区别
- 两种类型的区别是:
基本类型存储在内存栈中;
引用类型将数据地址存储在内存栈中,实际的数据内容存储在堆中。 - 典型的例子如下:
let a = [1,2], b = a
b.push(3)
console.log(a===b) // true
由于a,b保存的地址相同,所以对于数据内容的更改不会影响到a与b的相等性。
判断数据类型的方式
- typeof
| 类型 | 结果 |
| :---------------------------- | :------------ |
| Undefined |
"undefined"| | Null |"object"| | Boolean |"boolean"| | Number |"number"| | BigInt |"bigint"| | String |"string"| | Symbol |"symbol"| | Functin |"function"| | 其他任何对象(Object,Array) |"object"|
typeof能够精准的识别基本类型,但对于引用类型无法进行区分。
- instanceof 一般用于判断引用类型。
[1,2] instanceof Array //true
判断简单类型时会出现如下错误:
var str = 'hello world'
str instanceof String // false
var str1 = new String('hello world')
str1 instanceof String // true
- Object.prototype.toString 可以做到精准识别。
Object.prototype.toString.call(99) //"[object Number]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call({}) //"[object Object]"
原型与原型链
先来一张图,给个直观理解(个人认为比高程之类的书上给的要简单易懂哈哈哈)
莫嫌字丑哈哈哈,用平板画的。
原型
可以看到图上的 #605 #809 就是原型。
原型在哪里?
window对象下面挂载了许多内置函数:Object,Array,Function等等,我们习惯称他们为构造函数。而每个构造函数都有一个prototype属性,prototype的指向就是我们一直说的原型。
原型是什么?
原型的本质其实就是一个对象。它提供了对应类型的共同方法和属性。
如:Array.prototype原型,提供了数组独有的push,pop方法;
Function.prototype原型,提供了函数类型特有的call方法。
如何找到原型?
- 利用
__proto__属性(不推荐)
[1,2,3].__proto__ === Array.prototype //true
- 利用
Object.getPrototypeof(instance)
Object.getPrototypeOf([1,2]) === Array.prototype //true
原型链
为什么我们从没有未数组声明过方法,但却可以调用push,pop等方法呢?
const arr = []
arr.push(1) // [1]
逐级查找
js在当前对象上找不到对应的方法后,会沿着原型链逐层查找。
例如:
const arr = []
arr.push(1) // [1]
arr.hasOwnProperty("keys") //true
arr.sayHi() //Uncaught TypeError: arr.sayHi is not a function
- arr上没有push方法;沿原型链找到数组原型Array.prototype上存在push方法,则调用。
- arr上没有hasOwnProperty;沿原型链找到数组原型Array.prototype,也没有hasOwnProperty方法;继续沿原型链查找,由于"数组原型"本质上也是一个对象实例,所以"数组原型"的原型是Object.prototype,发现存在hasOwnProperty,则调用。
- arr上、数组原型、对象原型上都没有这个方法;而对象原型是原型链的尽头
Object.getPrototypeof(Object.prototype) === null,所以这个方法不存在,直接报错。
JavaScript中的OOP
new做了什么
function Person(name){ this.name = name }
Person.prototype.say = function() { console.log( this.name )}
let p = new Person("Lee")
- 创建一个空对象
let obj = {} - obj的__proto__指向构造函数的ptototype
obj.__proto__ = Person.prototype - 执行Person.call(obj)
- 若构造函数显式return一个对象则返回该对象;否则返回obj。
寄生组合模式
function Person(name){ this.name = name }
Person.prototype.say = function() { console.log( this.name )}
function Children(age) {
Person.call(this);
this.age = age
}
Children.prototype = Object.create(Parent.prototype,{constructor: Children})
/*
等价于
Child.prototype.__proto__ = Person.prototype;
Child.prototype.constructor = Child;
*/
class模式
class Person{
constructor(name){
this.name = name
}
say(){ console.log(this.name) }
}
/*
extends 等价于
Child.prototype.__proto__ = Person.prototype;
Child.prototype.constructor = Child;
*/
class Children extends Person{
constructor(name, age){
super(name) //等价于 Parent.call(this,name);
this.age = age
}
}
AJAX
- 全称:async JavaScript and xml
js通过异步通讯获得服务端的xml更新页面
原生ajax:
const xhr = XMLHttpRequest()
xhr.open('GET','/')
xhr.onreadystatechange = ()=>{
if(xhr.readyState===4 && xhr.status===200){
console.log(xhr.responseText)
}
}
xhr.send()
简化版ajax:
const xhr = XMLHttpRequest()
xhr.open('GET','/path?query=x')
xhr.onload(()=>{console.log(xhr.responseText)})
xhr.onerrer(()=>{console.log('error')})
xhr.send()
事件机制
事件捕获:先父后子,先外后里。
事件冒泡:先子后父,先里后外。
- 阻止冒泡:event.stopPropagation()
- addEventListener("click", fn , false):第三个参数为是否在捕获阶段执行。默认false,即在冒泡阶段执行。
正则相关
ch2 - 运行机制
变量提升
- 之所以需要实现变量提升,是因为JS机制:先编译,再运行。
- 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值被设为undefined;在代码执⾏阶 段,JavaScript引擎会从变量环境中去查找定义的变量和函数。
- 如果在编译阶段,存在两个函数名相同的函数定义,那么最终存放在变量环境中的是最后定义的那个。
- 函数提升要比变量提升的优先级要高一些,且不会被变量声明覆盖,但是会被变量赋值之后覆盖。
showName()
var showName = function() { console.log(2) }
function showName() { console.log(1) }
showName() // 1 , 2
/ *** 等价于 *** /
function showName() { console.log(1) }
var showName //函数提升不会被变量声明覆盖
showName()
showName = function() { console.log(2) }
调用栈和执行上下文
- 每调用一个函数,JS引擎则编译对应的函数代码并创建执行上下文(包括变量环境对象、词法环境对象、this引用),压入调用栈。随后由JS引擎执行代码
- 函数执行完毕后,清理函数内变量占用的内存,将该执行上下文弹栈。
- 当调用栈中的执行上下文个数达到一定数量后会发生堆栈溢出。
作用域
定义:作用域是指在程序中定义变量的区域。
全局作用域
js任何位置都可以访问到全局作用域中的变量
函数作用域
函数内部定义的变量或者函数只能在函数内部被访问。函数执⾏结束之后被销毁。
块级作用域
- 函数内部通过
var声明的变量,在编译阶段全都被存放到变量环境⾥⾯了。 - 通过
let声明的变量,在编译阶段会被存放到词法环境中。 - 函数内的块级作用域中,通过let声明的变量被集中存储在一个区域内,并被压入词法环境对象的顶部。(词法环境内部,维护了⼀个⼩型栈结构。)
4. 暂时性死区:在块级作用域中,若变量使用
let / const 声明,则在声明之前使用该变量会报错。
作用域链
词法作用域:
作用域是由代码中函数声明的位置决定的,是静态的。 作用域链由词法作用域决定。
-
每个函数声明时会创建一个函数声明对象,该对象的
[[scope]]属性指向声明该函数的执行上下文。 -
每个变量环境包含一个
outer属性,指向外部执行上下文(该函数的声明对象中的[[scope]]属性对应的上下文)。 -
全局上下文中变量环境的outer属性为
null,即全局上下文是作用域链的终点。
作用域链定义:
- 定义:本质上是一个执行上下文的指针列表。
- 作用:保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链可以访问到外层环境的变量和函数。
变量查找方式:
- 词法环境对象栈顶
- ==> 词法环境对象栈底
- ==> 变量环境对象
- ==> outer对应的执行上下文
- ==> 按作用域链进行查找,直到全局执行上下文:outer为null
闭包
- 定义:
闭包 = 函数 + 对其词法环境的引用。(MDN官方定义)
由于函数声明的函数对象[[scope]]属性会记录外层词法环境的特点,可以通过return一个函数来保留当前执行上下文中的变量。
- 用途:
- 闭包的第一个用途是:使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
- 闭包的另一个用途是:使已经运行结束的函数上下文中的函数对象继续留在内存中,因为函数对象的[scope]属性保留了对词法环境的引用,所以这个词法环境不会被回收。
- setTimeout 等回调函数。
this用法
-
箭头函数:不会创建执行上下文,没有自己的this。
let obj = { num: 1, prin: function(){ //父级作用域 此处保存着父级的this setTimeout(()=>{ console.log(this.num); },1000) } } obj.prin() // 1 //实际上就是闭包的应用。 -
绑定丢失:隐式绑定作为回调函数时,会出现绑定丢失的情况。
let user = { firstName: "John", sayHi() { alert(`Hello, ${this.firstName}!`); } }; setTimeout(user.sayHi, 1000); // Hello, undefined!
EventLoop
- 可以参考我的个人博客:juejin.cn/post/692416…
- 附两张参考图:
ch3 - ES6
let和var的区别
- let块级作用域
- let不存在变量提升(function和var都存在变量提升,function优先)
- 存在暂时性死区,如果在变量声名前使用会报错
- let不允许重复声名
- 在全局上下文中声明的var变量会成为window的属性;let不会
- var变量在执行上下文的变量环境对象中注册,let const变量在执行上下文的词法环境对象中注册。
promise
解构赋值
-
交换变量
[a,b] = [b,a] -
参数处理
function (args){ const {num,sum} = args }
String
-
模板字符串
`名字:${name}` -
includes,startsWith,endsWith
Array
-
Array.from
-
arr.fill
-
arr.includes
-
arr.flat
arr.flat(n) //n为想拉平的层数 [1, [2, [3]]].flat(Infinity) //拉平无限层,则拉平为一维数组
Object
- Object.assign( target, obj1, obj2 ) 浅复制
- Object.getPrototypeof( obj )
ch4 - 编程
数组相关
数组的变更方法:在原数组上进行更改
- sort():可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。
- reverse():交换次序
- splice():插入/删除元素方法
- push,pop,shift,unshift
数组的替换方法:根据原数组返回新数组
- concat() :数组连接
arr.concat([2,3]) - slice():用于截取数组中的一部分返回
arr.slice(0,5) - indexOf() 和 lastIndexOf() 迭代方法
- every()、some()、filter()、map() 和 forEach() 方法:对数组项处理,返回结果
- reduce方法:将数组中的值整合为一个值。
let arr = [1,2,3] arr.reduce((sum,v,idx,a) => sum+v*v, 0) //14
数组去重
- indexOf + reduce
let fn = arr => arr.reduce((account,v)=>{
if(account.indexOf(v)===-1) account.push(v)
},[])
- indexOf + filter
let fn = arr => arr.filter((v,index)=>arr.indexOf(v)===index)
- Set
let fn = arr => [...new Set(arr)]
- Map (key的唯一性)
let fn = arr =>{
let map = new Map()
arr.forEach(v=>map.set(v,true))
return [...map.keys()]
}
对象相关
对象的原生方法
- Object.create( obj ):以obj为原型,创建一个对象
- Object.assign( target, obj1, obj2 ):将obj1,obj2合并到target中
- Object.getPrototypeof(obj):获得原型
- Object.keys( obj ):获取对象的属性列表
- Object.values( obj ):获取对象的值列表
深浅拷贝
浅拷贝
只进行浅层拷贝,若对象上的属性仍是引用类型则无法实现完全拷贝。
//method 1
let a = { id:1 }
let new_A = Object.assign( {}, a )
//method 2
let a = { id:1 }
let b = {...a}
let a = 1
let b = a
深拷贝
递归进行完全拷贝
JSON.parse(JSON.stringify( obj ))- 通过递归函数
let deepClone = (obj)=>{ if (typeof obj !== "object" && typeof obj !== "function") throw new Error(); else { let newObj; if (Array.isArray(obj)) newObj = [...obj]; else newObj ={...obj}; for (let i in newObj) { if (typeof newObj[i] !== "object") continue; else newObj[i] = deepClone(newObj[i]); } return newObj; }
真题总结
Object.defineProperty
-
configurable(可改变):
当且仅当该属性的configurable键值为true时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 默认为false。 -
enumerable(可枚举):
当且仅当该属性的enumerable键值为true时,该属性才会出现在对象的枚举属性中。默认为false。 数据描述符还具有以下可选键值: -
value(值):
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。 -
writable(可写):当且仅当该属性的
writable键值为true时,属性的值,也就是上面的value,才能被赋值运算符改变。默认为false。
存取描述符还具有以下可选键值:
-
get:
属性的 getter 函数,如果没有 getter,则为undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined。 -
set:
属性的 setter 函数,如果没有 setter,则为undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this对象。默认为undefined。
对象属性遍历
- Object.keys(obj).forEach(v=>{ console.log(obj[v]) })
- 使用
for in方式。注意:for in 方式会同时输出Object.prototype上的属性for (let key in obj) { console.log(obj[key]) }
写在最后
上篇写过后,公司的项目突然忙了起来,耽误了许久。前段时间的复习热情有些凉了下来。
说实话对于前路还是非常迷茫的。我不知道是否每个前端er都心里清楚且坚定自己要什么。
但我现在知道的是:我是真实地对前端热情充沛,所以我会告诉自己坚定下去,努力下去。