最近负责前端技术一面,提问他人的时候,会发现js基础知识好多细节自己也不是很扎实,特意找时间统一梳理了一下:
1. JS数据类型
js数据类型分为:基础数据类型(7种)和引用数据类型object。
基础数据类型: null、undefined、 number、 string、 boolean 、symbol(es6)、 bigint(可以表示大于2^53 -1的整数)
引用数据类型:object。object包括array、function、Date、RegExp等
2. 判断数据类型的方式:
(1) typeof:
用来判断除了null的基本数据类型,typeof null返回object.
引用数据类型除function外,都返回object
var a = 1;
var b = 'lala';
var c = true;
var d = undefined;
var e = Symbol();
var f = 1n;
var g = null;
var h = {};
var i = [];
var j = function() {};
console.log(typeof a); // number
console.log(typeof b); // string
console.log(typeof c); // boolean
console.log(typeof d); // undefined
console.log(typeof e); // symbol
console.log(typeof f); // bigint
console.log(typeof g); // object
console.log(typeof h); // object
console.log(typeof i); // object
console.log(typeof j); // function
复制代码
(2)instanceof
用来判断引用数据类型,类实例,instanceof会沿着原型链一直往上寻找,返回值为true或false
var h = {};
var i = [];
var j = function() {};
var k = new Date();
var l = /[0-9]/g;
console.log(h instanceof Object); // trye
console.log(i instanceof Array); // true
console.log(j instanceof Function); // true
console.log(k instanceof Date); // true
console.log(l instanceof RegExp); // true
复制代码
(3) Object.prototype.toString.call()
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call('lala');// "[object String]"
Object.prototype.toString.call(1);// "[object Number]"
Object.prototype.toString.call(true);// "[object Boolean]"
Object.prototype.toString.call(new Date());// "[object Date]"
Object.prototype.toString.call(/[0-9]/);// "[object RegExp]"
复制代码
手写instanceof
function myInstanceof(left, right) {
// 这里先用typeof来判断基础数据类型,如果是,直接返回false
if(typeof left !== 'object' || left === null) return false;
// getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(left);
while(true) { //循环往下寻找,直到找到相同的原型对象
if(proto === null) return false;
if(proto === right.prototype) return true;//找到相同原型对象,返回true
proto = Object.getPrototypeof(proto);
}
}
// 验证一下自己实现的myInstanceof是否OK
console.log(myInstanceof(new Number(123), Number)); // true
console.log(myInstanceof(123, Number));
复制代码
3. 深浅拷贝
这要从数据的在内存中存储的位置说起,基本数据类型存储在栈中,引用数据类型数据存储在堆中,栈中存储了访问对象的地址。
对于基础数据类型来讲,都是对值的拷贝;
但是对引用数据类型来讲,分为深拷贝和浅拷贝。
浅拷贝首先会创建一个对象,对原对象的属性值精准拷贝,如果属性值是基础数据类型,拷贝的就是基础数据类型的值,如果属性值是引用数据类型,拷贝的是内存地址。
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。也就是说是在堆内存中重新开辟空间,拷贝后数据存放在新的地址,同时指针指向新的地址,与原数据完全隔离。
常见的浅拷贝的实现方式有:
object.assign(target, …sources)
扩展运算符 …
concat 拷贝数组
slice 拷贝数组 arr.slice(begin, end);
// Object.assign()
let target = { a: 1 }
let source = { b: 2, c: { d: 3 } };
Object.assign(target, source);
console.log(target); // { a: 1, b: 2, c: { d: 3 } };
target.b = 5;
target.c.d = 4;
console.log(source); // { b: 2, c: { d: 4 } };
console.log(target); // { a: 1, b: 5, c: { d: 4 } };
// ...
let obj = { a:1, b: { c:1 } }
let obj2 = { ...obj }
let arr = [1, 2, 3];
let newArr = [...arr];
// concat()
let arr = [1, 2, 3];
let newArr = arr.concat();
// slice()
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
复制代码
但是使用 object.assign 方法有几点需要注意:
- 它不会拷贝对象的继承属性;
- 它不会拷贝对象的不可枚举的属性;
- 可以拷贝 Symbol 类型的属性。
let obj1 = { a:{ b:1 }, sym:Symbol(1)};
Object.defineProperty(obj1, 'innumerable' ,{
value:'不可枚举属性',
enumerable:false
});
let obj2 = {};
Object.assign(obj2,obj1)
obj1.a.b = 2;
console.log('obj1',obj1); // { a: { b: 2 }, sym: Symbol(1), innumerable: '不可枚举属性'}
console.log('obj2',obj2); // { a: { b: 2 }, sym: Symbol(1)}
复制代码
深拷贝的实现方式:JSON.stringify
但是使用 JSON.stringify 实现深拷贝还是有一些地方值得注意,总结下来主要有这几点:
- 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;
- 拷贝 Date 引用类型会变成字符串;
- 无法拷贝不可枚举的属性;
- 无法拷贝对象的原型链;
- 拷贝 RegExp 引用类型会变成空对象;
- 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
- 无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。
function Obj() {
this.func = function () { alert(1) };
this.obj = {a:1};
this.arr = [1,2,3];
this.und = undefined;
this.reg = /123/;
this.date = new Date(0);
this.NaN = NaN;
this.infinity = Infinity;
this.sym = Symbol(1);
}
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{
enumerable:false,
value:'innumerable'
});
console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);
复制代码
手写浅拷贝
const shallowClone = (target) => {
if (typeof target === 'object' && target !== null) {
let cloneTarget = Array.isArray(target) ? [] : {};
for(let prop in target) {
if (target.hasOwnProperty(prop)) { // 遍历对象自身可枚举属性(不考虑继承属性和原型对象
cloneTarget[prop] = target[prop]
}
}
return cloneTarget
} else {
return target
}
}
复制代码
手写深拷贝
const deepClone = (target) => {
if (typeof target === 'object' && target !== null) {
let cloneTarget = Array.isArray(target) ? [] : {};
for(let prop in target) {
if (target.hasOwnProperty(prop)) { // 遍历对象自身可枚举属性(不考虑继承属性和原型对象
cloneTarget[prop] = deepClone(target[prop]); //递归导致日期、正则变成{}
}
}
return cloneTarget
} else {
return target
}
}
复制代码
改进版深拷贝
考虑日期、正则、循环引用
const deepClone = (target, map = new WeakMap()) => {
if (target === null) return null
if (typeof target !== 'object' || target.constructor === Date || target.constructor === RegExp) return target
if (map.has(target)) return target // 解决循环引用
const deepTarget = Array.isArray(target) ? [] : {};
map.set(target, true)
for (let prop in target) {
if (target.hasOwnProperty(prop)) { // 遍历对象自身可枚举属性(不考虑继承属性和原型对象
deepTarget[prop] = deepClone(target[prop], map)
}
}
return deepTarget
}
复制代码
4. 原型
原型对象
首先思考三个问题:
- 什么是原型对象?
- 原型对象何时产生?
- 原型对象如何访问?
什么是原型对象?
原型对象本质就是一个对象,所有函数都有prototype属性,该属性指向函数的原型对象,原型对象中包括:
- constructor: 指向构造函数
- 继承自Object的属性和方法
原型对象何时产生?
原型对象在函数创建的时候产生,每声明一个函数,都会做以下操作:
- 浏览器会在内存中创建一个对象
- 对象中添加一个constructor属性
- constructor属性指向该函数
- 将新创建的对象赋值给函数的prototype属性
原型对象如何访问?
函数名.prototype
通过函数实例的__proto__属性,函数每创建一个实例,该实例内部包含一个指针,指向构造函数的原型对象。
5. 原型链
原型链就是访问实例上某个属性或者方法时,先在实例中查找,找到即返回;若没有,则通过__proto__属性到构造函数的原型对象prototype上去找,找到则返回;若没有,则继续到构造函数的原型对象prototype的__proto__属性查找,直到搜索到Object.prototype为止。
一张图理解原型、原型链
总结
所有函数都是Function的实例,Function也是Function的实例
Function继承Object,除Object外,其他一切皆继承自Object
6. 继承
有两个函数、函数A、函数B,实现函数A继承函数B的属性和方法:
原型链继承
A.prototype = new B()
A.prototype.constructor = A
var a1 = new A()
var a2 = new A()
复制代码
缺点:
- 父类B函数原型对象的引用类型属性会被实例 a1 a2共享
- 创建子类A的实例无法向父类B传参数
- 子类无法实现继承多个函数,这里A只能继承自B
构造函数继承
function A(e) {
B.call(this, e)
}
复制代码
缺点:
子类无法访问父类原型上的属性和方法
组合继承
function B (name, age) {
this.name = name
this.age = age
}
B.prototype.setName = function (name) {
this.name = name
}
function A (name, age, price) {
B.call(this, name, age)
this.price = price
}
A.prototype = new B ()
A.prototype.constructor = A
A.prototype.setPrice = function (price) {
this.price = price
}
var sub = new A()
复制代码
缺点:
调用了两次B():一次是B.call();一次是new B()
寄生组合继承
Object.create 方法,这个方法接收两个参数:一是用作新对象原型的对象、二是为新对象定义额外属性的对象(可选参数)。
function B (name, age) {
this.name = name
this.age = age
}
B.prototype.setName = function (name) {
this.name = name
}
t
function A (name, age, price) {
B.call(this, name, age)
this.price = price
}
// 第一种写法
// 创建一个对象{},并且把对象的_proto_赋值为Object.create 的参数
// A.prototype.__proto__ = B.prototype
A.prototype = Object.create(B.prototype, {consturctor: A})
// 第二种写法
//var F = function () { } //核心代码
//F.prototype = B.prototype; //核心代码
//A.prototype = new F();
//A.prototype.contructor = A
A.prototype.setPrice = function (price) {
this.price = price
}
var sub = new A()
复制代码
ES6 Class类
class B {
static mood = 'good' // 静态属性
constructor () {
this.money = 1000000
}
buybuybuy () {
this.money -= 100
console.log('money', this.money)
}
}
class A extends B {
super()
}
var a1 = new A()
a1.buybuybuy()
复制代码
7. 执行上下文(Execution Context)
浏览器获取到源代码后,主要做了几个事情:
- 分词/词法分析():将代码进行分割,生成token;
- 解析/语法分析():按照语法将token转换成AST抽象语法树;
- 可执行代码:解析器生成字节码,逐行解释执行,分析器监控热点代码,编译器将热点代码编译为机器码。
什么是执行上下文?
执行上下文,又称执行上下文环境。执行上下文分为三种类型:
- 全局执行上下文:程序开始时,会创建全局执行上下文,并压入执行栈中。
- 函数执行上下文:当函数被调用时创建函数执行上下文,并将函数压入执行栈中。
- eval执行上下文:eval函数专有的执行上下文。
执行上下文分两个阶段:创建阶段和执行阶段。
创建阶段
执行上下文主要由两部分组成:词法环境和变量环境。
词法环境(LexicalEnvironment)
词法环境分类:全局、函数、模块
词法环境构成:
-
环境记录(Environment Record):存放、初始化变量
声明式环境记录(Declarative Environment Record): 存放直接用标识符定义的元素,比如const let声明的变量 对象式环境记录(Object Environment Record):主要用于with的语法环境。 复制代码
-
外部环境(Outer):创建作用域链,访问父词法作用域的引用
-
thisBinding:确定当前环境中this的指向
变量环境(variableEnvironment)
也是一个词法环境。主要的区别在于通过var 声明的变量以及函数声明存放在变量环境。
简单来说,执行上下文创建阶段主要做了三件事情:
- 初始化变量、函数、形参
- 创建作用域链
- 绑定this
executionContext = {
variableObject: {
arguments: {
},
name: undefined,
getData: undefined
}, // 初始化变量、函数、形参
scopeChain: {}, // 创建作用域链
this: {} // 绑定this
}
复制代码
执行阶段
执行阶段主要做了两件事:
- 分配变量、函数的引用、赋值
- 执行代码
变量提升 vs 暂时性死区
var声明的变量以及函数声明,在执行上下文创建阶段,已经初始化完成,并赋值为undefined,代码未执行到var赋值行,也可以访问var定义的变量,值为undefined,这种现象被称作变量提升
相反,由const、let声明的变量,在词法环境中,初始化时会被置为标志位,在代码没执行到let、const赋值行时,提前读取变量会报错,这个特性叫做暂时性死区。
执行上下文栈
浏览器的JS解释器是单线程的,相当于浏览器在同一时间只能做一件事。
代码中只有一个全局执行上下文,和无数个函数执行上下文,组成了执行上下文栈。
一个函数的执行上下文,在函数执行完毕后会被移除执行栈。
8. 作用域
作用域的主要用途是隔离变量和函数,并控制它们的生命周期。主要分为三种类型:
- 全局作用域
- 函数作用域
- 块级作用域
作用域是在执行上下文创建时定义的,不是在代码执行时创建的,因此又称为词法作用域。
词法作用域 vs 动态作用域
词法作用域语动态作用域的区别是,词法作用域是执行上下文创建阶段阶段就定义的,动态作用域是指代码执行阶段创建的。
为了更好的理解JS采用的是词法作用域,看一下例子:
var name = 'xuna'
function getName() {
console.log(name)
}
function getName1() {
var name = 'na.xu'
return getName()
}
getName1() // xuna
复制代码
作用域链
当一个函数嵌套另一个函数时,在当前执行上下文环境的词法环境和变量环境的环境记录(Environment Record)中无法找到某个变量,就会通过外部环境(Outer)去访问父词法作用域,如果还没找到,就一层一层向上寻找,直到找到该变量或抵达全局作用域为止,这样的链式关系称为作用域链。
9. 闭包
闭包一般发生在函数嵌套时,内部函数访问外部函数的变量。
高级程序设计三中:闭包是指有权访问另一个函数作用域中的变量的函数,可以理解为(能够读取其他函数内部变量的函数)
再理解一下:
一个函数执行完,被弹出执行栈,当前执行上下文中不能直接访问被弹出栈的函数的词法作用域,而另一个函数中还保留了对该函数词法作用域的引用,这个引用就是闭包。
闭包的应用
封装私有变量
function Person() {
var money = 10000
return buy() {
money -= 1
}
}
var person = new Person()
person.buy() // money为person的私有变量,只能通过buy()修改
复制代码
缓存数据
function getDataList() {
let data = null
return {
getData() {
if(data) return Promise.resolve(data)
return fetch().then(res => data = res.json())
}
}
}
const list = getDataList()
list.getData()
复制代码