js内功修炼-基础篇

最近负责前端技术一面,提问他人的时候,会发现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 实现深拷贝还是有一些地方值得注意,总结下来主要有这几点:

  1. 拷贝的对象的值中如果有函数、undefined、symbol 这几种类型,经过 JSON.stringify 序列化之后的字符串中这个键值对会消失;
  2. 拷贝 Date 引用类型会变成字符串;
  3. 无法拷贝不可枚举的属性;
  4. 无法拷贝对象的原型链;
  5. 拷贝 RegExp 引用类型会变成空对象;
  6. 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;
  7. 无法拷贝对象的循环应用,即对象成环 (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);
复制代码

image.png

手写浅拷贝

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
  }
}
复制代码

image.png

改进版深拷贝

考虑日期、正则、循环引用

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. 原型

原型对象

首先思考三个问题:

  1. 什么是原型对象?
  2. 原型对象何时产生?
  3. 原型对象如何访问?

什么是原型对象?

原型对象本质就是一个对象,所有函数都有prototype属性,该属性指向函数的原型对象,原型对象中包括:

  • constructor: 指向构造函数
  • 继承自Object的属性和方法

原型对象何时产生?

原型对象在函数创建的时候产生,每声明一个函数,都会做以下操作:

  • 浏览器会在内存中创建一个对象
  • 对象中添加一个constructor属性
  • constructor属性指向该函数
  • 将新创建的对象赋值给函数的prototype属性

原型对象如何访问?

函数名.prototype
通过函数实例的__proto__属性,函数每创建一个实例,该实例内部包含一个指针,指向构造函数的原型对象。

5. 原型链

原型链就是访问实例上某个属性或者方法时,先在实例中查找,找到即返回;若没有,则通过__proto__属性到构造函数的原型对象prototype上去找,找到则返回;若没有,则继续到构造函数的原型对象prototype的__proto__属性查找,直到搜索到Object.prototype为止。

一张图理解原型、原型链

image.png

总结

所有函数都是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()
复制代码

image.png

缺点:

  1. 父类B函数原型对象的引用类型属性会被实例 a1 a2共享
  2. 创建子类A的实例无法向父类B传参数
  3. 子类无法实现继承多个函数,这里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 声明的变量以及函数声明存放在变量环境。

简单来说,执行上下文创建阶段主要做了三件事情:

  1. 初始化变量、函数、形参
  2. 创建作用域链
  3. 绑定this
executionContext = {
    variableObject: {
    	arguments: {
			},
      name: undefined,
      getData: undefined
    },  // 初始化变量、函数、形参
    scopeChain: {},  // 创建作用域链
    this: {} // 绑定this
}
复制代码

执行阶段

执行阶段主要做了两件事:

  1. 分配变量、函数的引用、赋值
  2. 执行代码

变量提升 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()
复制代码
柯里化
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享