JS高阶(一)V8引擎中的堆栈内存及前端必备的数据结构

179 阅读16分钟

V8引擎中的堆栈内存及前端必备的数据结构

1、ECMAScript262中数据类型的标准定义

  • ECMAScript is object-based: basic language and host facilities are provided by objects, and an ECMAScript program is a cluster of communicating objects. In ECMAScript, an object is a collection of zero or more properties each with attributes that determine how each property can be used—for example, when the Writable attribute for a property is set to false, any attempt by executed ECMAScript code to assign a different value to the property fails. Properties are containers that hold other objects, primitive values, or functions. A primitive value is a member of one of the following built-in types: Undefined, Null, Boolean, Number, String, and Symbol; an object is a member of the built-in type Object; and a function is a callable object. A function that is associated with an object via a property is called a method.

    ECMAScript是基于对象的:基本语言和主机设施是由对象提供的,而ECMAScript程序是通信对象的集群。在ECMAScript中,对象是0个或多个属性的集合,每个属性都具有决定如何使用每个属性的属性——例如,当属性的可写属性被设置为false时,执行ECMAScript代码为属性分配不同值的任何尝试都将失败。属性是容纳其他对象、基本只或函数的容器。原始值是以下内置类型之一的成员:未定义、Null、布尔、数字、字符串、和符号;对象是内置类型对象的成员;函数是可调用对象。通过属性与对象关联的函数成为方法。

  • ECMAScript defines a collection of built-in objects that round out the definition of ECMAScript entities. These built-in objects include the global object; objects that are fundamental to the runtime semantics of the language including Object, Function, Boolean, Symbol, and various Error objects; objects that represent and manipulate numeric values including Math, Number, and Date; the text processing objects String and RegExp; objects that are indexed collections of values including Array and nine different kinds of Typed Arrays whose elements all have a specific numeric data representation; keyed collections including Map and Set objects; objects supporting structured data including the JSON object, ArrayBuffer, and DataView; objects supporting control abstractions including generator functions and Promise objects; and, reflection objects including Proxy and Reflect.

    ECMAScript定义了一个内置对象集合,它完善了ECMAScript实体的定义。这些内置对象包括全局对象;对象是语言运行时语义的基础,包括对象、函数、布尔值、符号和各种错误对象;表示和操作数值(包括数学、数字和日期)的对象;文本处理对象字符串和RegExp;对象,该对象是值的索引集合,包括数组和9种不用类型的数组,其他元素都具有特定的数值数据表示;键控集合,包括Map和Set对象;支持结构化数据的对象,包括JSON对象、ArrayBuffer和DataView;支持控制抽象的对象,包括生成器函数和承诺对象;以及反射对象,包括代理和反射。

原始值类型【值类型/基本数据类型】

  • number 数字
  • string 字符串
  • boolean 布尔
  • null 空对象指针
  • undefined 未定义
  • symbol 唯一值
  • bigint 大数

对象类型【引用数据类型】

  • 标准普通对象 object
  • 标准特殊对象 Array、RegExp、Date、Math、Error...
  • 非标准特殊对象 Number、String、Boolean...
  • 可调用/执行函数 [函数] function

原始值类型详情

  • number数字类型:NaN非有效数字、Infinity无穷大的值

    • 数值中有最大安全数和最小安全数

      Number.MAX_SAFE_INTEGER; //9007199254740991 最大安全数字

      Number.MIN_SAFE_INTEGER;//-9007199254740991 最小安全数字

    • NaN!==NaN(NaN不等于任何数,包括自己)isNaN([value]);检测[value]是否为有效数字

    • Object.is(NaN,NaN);结果为true

  • string字符串类型:' '," ",``包起来的都是字符串

  • boolean布尔类型:true、false

  • null 后期会赋值,初始值就为null

  • undefined 后期并没有打算赋值,初始值就为undefined

  • symbol唯一值类型

    console.log(Symbol() === Symbol()); //false 创建了两个唯一值

    console.log(Symbol('AA') === Symbol('AA')); //false

    @作用1:可以给对象设置唯一值类型的属性「对象“属性名”的类型:string、symbol」

    let obj = {
    
    name: 'zhufeng',
    
    age: 12,
    
    [Symbol()]: 100
    
    };
    
    console.log(obj[Symbol()]); //undefined 获取的时候,它会创建一个新的唯一值,所以它获取的是一个新的唯一值的属性值 ,唯一值是不想等的,所以会返回undefined;
    

    [ ]的作用:为了让语法符合规范


    let a = [10, 20];
    let sym = Symbol('BB');
    let obj = {
       name: 'zhufeng',
       age: 13,
       0: 100,
       [{ xxx: 'xxx' }]: 200,  // "[object Object]":200
       [a]: 300, // "10,20":300
       [Symbol('AA')]: 400,
       [sym]: 500
    };
    

    Object.getOwnPropertySymbols(obj) 获取当前对象所有Symbol类型的私有属性,结果为数组

    Reflect.ownKeys(obj);获取所有私有属性,不论是啥类型

    let obj = {
    
    name: 'zhufeng',
    
    age: 12,
    
    [Symbol()]: 100
    
    };
    
    let symbolKeys = Object.getOwnPropertySymbols(obj); //获取当前对象所有Symbol类型的私有属性,结果为数组
    
    symbolKeys.forEach(key => {
    
    console.log(obj[key])
    
    }
    // for (let key in obj) { //问题:for in循环是无法迭代symbol类型的私有属性
    //     console.log(key);
    // }
    // ----
    // let keys = Object.keys(obj); //获取非symbol类型的私有属性(返回包含属性名的数组)
    // keys = keys.concat(Object.getOwnPropertySymbols(obj)); //获取symbol类型的私有属性
    // console.log(keys);
    // ----
    // let keys = Reflect.ownKeys(obj); //获取所有私有属性,不论是啥类型
    // console.log(keys);
    
    • 面试题:

    let a = {
    
    name: 'zhufeng'
    
    };
    
    let b = {
    
    name: 'web'
    
    };
    
    let obj = {};
    
    obj[a] = 100; //obj["[object Object]"]=100
    
    obj[b] = 200; //obj["[object Object]"]=200
    
    console.log(obj[a]); //200 
    

    对象不能作为属性,它会转换成字符串,会调用toString()

    a.toString(); //"[object Object]"

    @作用2: vuex/redux中我们需要派发很多行为标识,我们可以把这些行为标识统一管理,为了保证行为标识的唯一性,所以可以基于symbol处理

    @作用3:

    把Symbol做为对象,提供的很多静态属性方法,是JS很多的知识的底层实现原理,Symbol.hasInstance\Symbol.toStringTag\Symbol.toPrimitive\Symbol.iterator...很多JS底层的处理机制,就是基于这些属性方法实现的

  • BigInt大数类型:数字后面加个n即是大数类型

    • 10 Number类型

    • 10n BigInt类型

    • !在js中进行数学运算时,如果计算的数值超过最大/最小安全数字,计算出来的结果很可能就不准确[如果遇到这样的需求,则一定会出问题:服务器端数据存储,是有longInt类型,服务器返回给客户端的值也是超过安全数字的{一般会以字符串的方式返回,这样保证返回途中数字的安全性},此时我们去进行数学运算,结果可能会错误]

      解决方法:把服务器返回的值变为BigInt格式,然后进行运算[保证了运算的准确性{进行运算的另外一个值也应该是BigInt类型}];把运算的结果变为字符串,再发送给服务器即可

      BigInt(“45431513451245134251245”);//45431513451245134251245n

      (45431513451245134251245n).toString();//"45431513451245134251245"


对象类型

  • 标准普通 {num:100}
  • 标准特殊 [10,20] /\d+/ new Date() new Error() ...
  • 非标准特殊 new Number(10)... 原始值类型的对象类型实例
  • 函数对象 function fn(){}

2、typeof数据类型检测的底层机制

  • UnaryExpression : typeof UnaryExpression Let val be the result of evaluating UnaryExpression. If Type(val) is Reference, then If IsUnresolvableReference(val) is true, return “undefined”. Let val be GetValue(val). ReturnIfAbrupt(val). Return a String according to Table 35.

    UnaryExpression: UnaryExpression的类型 设val为对UnaryExpression求值的结果。 如果类型(val)是引用,则 如果IsUnresolvableReference(val)为真,则返回“undefined”。 让val为GetValue(val)。 ReturnIfAbrupt (val)。 根据表35返回一个字符串。

Snipaste_2021-07-22_09-44-01.png

特点1:返回的结果是字符串,字符串中含有了对应的数据类型

  • typeof typeof typeof[1,2,3];

    typeof[1,2,3] //"object"

    typeof "object" //"string"

    typeof "string" //"string"

    多个typeof返回的都是string

特点2:按照计算机底层存储的二进制进行检测【效率高】

  • 计算机科学:计算机原理、进制转化、计算机网路、数据结构和算法...
  • 000 对象
  • 1 整数
  • 010 浮点数
  • 110 布尔
  • 000000... null
  • -2^30 undefined
  • ...

特点3:typeof null -> "object"

特点4:typeof 对象 -> “object” && typeof 函数 -> "function"

  • 验证是否是对象的判断

特点5:typeof 未被声明的变量 -> "undefined"

  • 插件封装中的暴露API

检测数据类型:

  • typeof

    • 返回结果是一个字符串,字符串中包含了对应的数据类型
    • typeof检测未被声明的变量,不会报错,结果是”undefined“
    • typeof null -> "object" ?
    • typeof 不能细分是啥对象「排除函数对象」,返回结果是 ”object/function“ ?
  • 所有的数据类型值,在计算机底层中都是以2进制形式存储的{64位}

  • 而typeof就是基于二进制检测的:如果是以“000”开始的二进制,则被识别为对象(null存储的二进制值都是0,符合以000开始);然后再去看对象是否实现了[[call]],实现了则为函数(返回“function”),没实现就是对象(返回“object”);

    好处:检测处理的性能是很高的

  • instacnceof

  • constructor

  • Object.prototype.toString.call([value])

  • Array.isArray([value])//特殊

需求:验证val是否为一个对象

if (val !== null && (typeof val === "object" || typeof val === "function")) { }

if(val !== null && /^(object|function)$/i.test(typeof val)){
	//...
}

支持浏览器导入 && 支持NODE端运行{CommonJS规范}

(function () {
  let utils = {
    // ...
  };
  /!* 暴露API *!/  typeof不会报错,只会返回undefinedif (typeof module === "object" && typeof module.exports === "object") module.exports = utils;(符合CommonJS规范)
  if (typeof window !== "undefined") window.utils = utils; (浏览器中有window这个对象,所以不会报错)
})(); 

课后思考题:0.1 + 0.2 ?== 0.3

  • 原因:计算机计算精准的问题。

    0.6 * 2 = 1.2 => 1

    0.2 * 2 = 0.4 => 0

    0.4 * 2 = 0.8 => 0

    0.8 * 2 = 1.6 => 1

    0.6 * 2 = 1.2 => 1

    ........

    所有值在计算机底层都是按照二进制存储的,运算也是基于二进制来的...

    整数运算一般不会有问题

    小数运算一般会出现问题

    计算机底层都是按照二进制值来存储数据的 【typeof】

    • 64位的二进制值
    • 对于无限循环的情况,会自动裁切掉多余的部分 => 十进制的小数,在计算机底层存储的时候,就已经”失真“了

    0.1 + 0.2 在进行运算的时候,是按照二进制来进行计算,把计算的结果变为十进制,交给客户端呈现,浏览器呈现的数值是有长度限制的,超过长度限制的也会截取掉【从最后一位向前数,全是零的干掉,遇到非零的就要保留了】

    ​ 浮点数转为二进制会出现“无限循环”的情况,计算机底层存储的时候按照可以识别的最长位数存储,其余的直接干掉... 所以浮点数存储的二进制值本身就失去了精准度,所以最后运算的结果也是缺乏精准度的,而且小数最后面的零会省略,但凡不是0,都不会省略 => 0.1+0.2=0.30000000000000004 0.1+0.6=0.7(0.70000000000....)


@1 十进制转换为二进制的计算 n.toString(2)

​ + 整数部分

​ + 小数部分

@2 JS使用Number类型表示数字(整数和浮点数),遵循IEEE-754标准 通过64位二进制值来表示一个数字

babbage.cs.qc.cuny.edu/IEEE-754.ol…

​ 第0位:符号位,0表示正数,1表示负数 S

​ 第1位到第11位「11位指数」:储存指数部分 E

​ 第12位到第63位「52位尾数」:储存小数部分(即有效数字)F

​ 注:尾数部分在规约形式下第一位默认为1(省略不写)

@3 最大安全数字「16位」 Number.MAX_SAFE_INTEGER === Math.pow(2,53)-1

@4 怎么解决精度问题?

​ + 将数字转成整数「扩大系数」

​ + 三方库:Math.js 、decimal.js、big.js ...

console.log((0.1x10 + 0.2x10)/10);//扩大系数,变为整数进行运算

编写plus和minus方法,可以解决浮点数加减运算中的精准度问题
  const plus = function plus(n,m){
...
};
const minus = function minus(n,m){
...
};
--------------------------------------
const coefficient = function coefficient(num) {
 num = num + '';
 let [, char = ''] = num.split("."),
     len = char.length;
    //pow() 方法返回 x 的 y 次幂。
 return Math.pow(10, len);

}

const plus = function plus(n, m) {
   n = +n;
 m = +m;
 if (isNaN(n) || isNaN(m)) return NaN;
 let coeffic = Math.max(coefficient(n), coefficient(m));
 return ((n * coeffic + m * coeffic) / coeffic);
};
const minus = function minus(n, m) {
 n = +n;
 m = +m;
 if (isNaN(n) || isNaN(m)) return NaN;
 let coeffic = Math.max(coefficient(n), coefficient(m));
 return ((n * coeffic - m * coeffic) / coeffic);
};

专题:JS中检测数据类型的方法

typeof[value]:检测数据类型的运算符

@1 返回结果是一个字符串,其次字符串中包含对应的数据类型,例如:"number","object","function"....

typeof typeof typeof[1,2,3] => "string"

@2 弊端
  • typeof null -> "object"
  • typeof检测对象,除函数对象会返回“function”,其余对象返回的都是“object”「不能细分对象」
  • typeof 未被声明的变量 -> 不会报错,而是"undefined"

除了这些以外,用typeof监视原始值类型【或者函数类型】还是非常方便、准确的

@3 检测的原理

所有数据类型在计算机底层都是按照二进制的值来进行存储的,而typeof就是按照二进制的值进行检测的

  • 性能好
  • 对象的二进制值开头都是“000”,而null的二进制值都是零,所以typeof检测null的时候,识别其实一个对象(这样是不对的);如果识别为对象,再看对象是否实现了call方法,实现了call方法的返回“function”,没有实现的统一返回“object”;

@4 应用

  • 检测除null之外的原始值类型可以使用他
  • 检测是否为对象 if(obj!==null && /^(object|function)$/.test(typeof obj)){...}
  • 检测某个东西是否兼容 if(typeof Symbol!=="undefined"){...}
  • ...

instanceof:检测当前实例是否属于这个类【临时拉来做数据类型检测】

@1 [对象] instanceof [构造函数] 检测对象是否为这个类的实例,基于这个特点可以“临时”拿来检测数据类型,返回true/false
@2 可以做一些数据类型的检测【对typeof做了一个补充,可以适当的细分一下对象】
@3 弊端:
  • 无法基于instanceof检测是否为标准普通对象【纯粹对象:直接是Object类的实例】;因为所有对象都是Object类的实例,基于“xxx instanceof Object”检测的时候,返回的结果都是true;

  • 对原始值类型无效:instanceof左侧只要是原始值类型,结果就是false,默认不会进行“装箱”

    例如:10 instanceof Number -> false

  • 检测的结果不一定严谨;因为可以修改原型链的指向

  • .....

@4 当我们基于“【value】instanceof [Ctor]”运算符进行数据类型的检测的时候
  • 传统版本:如果不存在这个函数,浏览器会按照当前【value】原型链一层一层向上找,直到找到Object.prototype为止;查看[Ctor].prototype是否出现在它的原型链中,如果出现了,则结果是true,说明[value]是[Ctor]的实例,反之则为false...

  • 新版本:首先调用【Ctor】【Symbol.hasInstance】(value)这个函数,如果存在这个函数,则直接基于这个函数处理即可当代浏览器基本都有,因为Symbol.hasInstance在Function.prototype中,每一个构造函数都是Function的实例,都可以调用Function.prototype[Symbol.hasInstance]这个方法,当我们基于 “[对象] instanceof [构造函数]”检测处理的时候,内部是这样处理的:

    ​ [构造函数][Symbol.hasInstance]([对象])

相关代码
let obj = {};
let arr = [];
let reg = /^$/;
let num = new Number(10);

console.log(arr instanceof Array); //true 
console.log(obj instanceof Array); //false
console.log(reg instanceof Array); //false
console.log(typeof num); //"object"
console.log(num instanceof Number); //true 说明num是Number类的一个实例「原始值对应的对象类型结果」

console.log(arr instanceof Array); //true 
console.log(arr instanceof Object); //true 
console.log(reg instanceof Object); //true 
console.log(obj instanceof Object); //true 
console.log(num instanceof Object); //true

//可以修改原型链的指向
const Fn = function Fn() {};
Fn.prototype = Array.prototype;
let f = new Fn;
console.log(f); //从结构来看,f一定不是数组「数组的结构:数字索引、逐级递增、length属性...」
console.log(f instanceof Array); //true

const Fn = function Fn() {
this.name = 'zhufeng';
};
Fn.prototype.sayName = function() {};
Fn.xxx = 'xxx';
Fn[Symbol.hasInstance] = function() { //这样设置是无效的
console.log(1);
return false;
};
Function.prototype[Symbol.hasInstance] = function() { //这样设置也是无效的
console.log(1);
return false;
};
let f = new Fn;
console.log(f instanceof Fn);

//------------------------------------

class Fn {
    name = 'zhufeng';
    sayName() {}
        // 当做对象,设置静态私有的属性方法「这样设置是有用的,所以重构“构造函数”的Symbol.hasInstance,只支持ES6中class创建的类,ES5中创建的构造函数不支持这样重构」
    static xxx = 'xxx';
    static[Symbol.hasInstance](obj) {
        console.log(obj);
        return false;
    }
}
let f = new Fn;
console.log(f instanceof Fn); //false
class Fn {
    name = 'Fn';
    x = 10;
    y = 20;
    sum() {
            if (!this.name) throw new TypeError('this.name is not defined');
            // ...
        }
        // 只要基于ES6中的class创建类,基于“static xxx”这种语法重写Symbol.hasInstance才有用;如果是ES5创建的类,基于 Fn[Symbol.hasInstance]=xxx 这样重写是无效的!!
    static[Symbol.hasInstance](obj) {
        return (obj.name && Object.getPrototypeOf(obj) === Fn.prototype) ? true : false;
    }
}
let f1 = new Fn;
let f2 = new Fn;
console.log(f1 instanceof Fn); //true
f1.sum();

let obj = {};
Object.setPrototypeOf(obj, Fn.prototype);
console.log(obj instanceof Fn); //Fn[Symbol.hasInstance](obj) false
// console.log(obj instanceof Fn); //true
// obj.sum(); //报错

@5 分析instanceof的优缺点和底层实现机制,并且重写instancof
/* 
instance_of:检测value是否为Ctor的实例
value:要检测的实例
Ctor:要检测的构造函数
*/
const instance_of = function instance_of(value, Ctor) {
 // 保证Ctor的格式也是正确的 要是一个函数,并且有prototype属性
 if (typeof Ctor !== "function") throw new TypeError('Right-hand side of instanceof is not callable');
 if (!Ctor.prototype) throw new TypeError('Function has non-object prototype in instanceof check');

 // 不支持原始值类型值的检测
 if (value === null) return false;
 if (!/^(object|function)$/.test(typeof value)) return false;

 // 支持Symbol.hasInstance方法的直接按照这个处理
 //if (typeof Ctor[Symbol.hasInstance] === "function") return Ctor[Symbol.hasInstance](value);
	if (typeof Symbol !== "undefined") return Ctor[Symbol.hasInstance](obj);

 // 不支持的则按照原型链一层层的查找即可 Object.getPrototypeOf(value):获取value所属构造函数的原型对象
 let proto = Object.getPrototypeOf(value);
 while (proto) {
     // Ctor.prototype出现在了value的原型链上「value是Ctor的实例对象」:直接返回true & 结束查找
     if (proto === Ctor.prototype) return true;
     proto = Object.getPrototypeOf(proto);
 }
 return false;
};

Object.getPrototypeOf(value):获取value所属构造函数的原型对象

Error 类型

除了通用的Error构造函数外,JavaScript还有6个其他类型的错误构造函数。更多客户端异常,详见 Exception Handling Statements

  • EvalError

    创建一个error实例,表示错误的原因:与 eval()有关。

    eval():函数会将传入的字符串当做 JavaScript 代码进行执行。

  • InternalError

    创建一个代表Javascript引擎内部错误的异常抛出的实例。 如: "递归太多".

  • RangeError

    创建一个error实例,表示错误的原因:数值变量或参数超出其有效范围。

  • ReferenceError

    创建一个error实例,表示错误的原因:无效引用。

  • SyntaxError

    创建一个error实例,表示错误的原因:eval()在解析代码的过程中发生的语法错误。

  • TypeError

    创建一个error实例,表示错误的原因:变量或参数不属于有效类型。

  • URIError

    创建一个error实例,表示错误的原因:给 encodeURI()decodeURI()传递的参数无效。


constructor:获取当前实例所属的构造函数【临时】

@1 [value].contructor 获取其构造函数,验证是否为我们想检测的类

​ 例如:[value].constructor===Array

@2 相比较于instanceof来讲
  • 可以检测原始值类型的值【排除null/undefined】
  • 检测是否为纯粹对象(标准普通对象) [value].constructor===Object
  • 和instanceof一样,检测的结果仅供参考【constructor这个值是可以被肆意修改的】
相关代码
//--------------constructor
let arr = [];
let reg = /^$/;
let num = 10;

console.log(arr.constructor === Array); //true
console.log(arr.constructor === Object); //false
console.log(reg.constructor === Array); //false
console.log(num.constructor === Number); //true

Object.prototype.toString.call([value]):专门检测数据类型的方法

@1 调用Object.prototype.toString方法,让方法中的this指向检测的值,就是检测当前值的数据类型

​ 返回的结果是一个字符串”[object ?]“

​ 例如:Object.prototype.toString.call(10) -> "[object Number]"

​ Object.prototype.toString.call(new Number(10)) -> "[object Number]"

​ 它是所有检测数据类型的办法中,最强大、最稳定...的方式{除了写起来麻烦一些}

@2 返回的结果是”[object ?]“,"?"会是啥呢?
  • 首先获取【value】[Symbol.toStringTag]属性值。如果存在这个属性,则这个属性值是什么,”?“就是什么

  • 如果没有这个属性,一般“?”是当前实例所属的构造函数

    Symbol.prototype & BigInt.prototype & Math & GeneratorFunction.prototype & Promise.prototype & Set.prototype & Map.prototype ... 这些类的原型上,都有Symbol.toStringTag这个属性

  • Object.prototype.toString这个方法是用来检测数据类型的,而且方法内部规定:方法中的this是谁,我们就检测谁的类型,所以我们基于call方法去改变this指向

  • 除Object.prototype.toString之外,其余构造函数原型上的toString一般都是用来“转换字符串的”,只有它是用来检测数据类型的,返回的结果 “[object ?]”

相关代码
//-------------Object.prototype.toString.call
let obj = {},
toString = obj.toString; //->Object.prototype.toString 基本上所有的数据类型,所属构造函数的原型上都有toString方法,一般都是用来转换为字符串的,只有Object.prototype.toString是用来检测数据类型的
console.log(toString.call(10)); //"[object Number]"
console.log(toString.call(new Number(10))); //"[object Number]"
console.log(toString.call("zhufeng")); //"[object String]"
console.log(toString.call(true)); //"[object Boolean]"
console.log(toString.call(null)); //"[object Null]"
console.log(toString.call(undefined)); //"[object Undefined]"
console.log(toString.call(Symbol())); //"[object Symbol]"
console.log(toString.call(10 n)); //"[object BigInt]"
console.log(toString.call({})); //"[object Object]"
console.log(toString.call([])); //"[object Array]"
console.log(toString.call(/^$/)); //"[object RegExp]"
console.log(toString.call(function() {})); //"[object Function]"
console.log(toString.call(new Date())); //"[object Date]"
console.log(toString.call(new Error())); //"[object Error]"
console.log(toString.call(Math)); //"[object Math]"
console.log(toString.call(function*() {})); //"[object GeneratorFunction]"
console.log(toString.call(Promise.resolve())); //"[object Promise]"
// 即使constructor值被修改 或者 基于Object.setPrototypeOf重定向实例的原型指向,结果也是不变的!!所以 toString.call 这种办法检测的结果是非常可靠的!!
[object ?] 返回自定义值
//自定义的构造函数一般返回的结果都是Object
// const Fn = function Fn() {};
// let f = new Fn;
// console.log(toString.call(f)); //“[object Object]”
// 需求:期望自己写自定义构造函数,所创建出来的实例在检测数据类型的时候,可以返回的是“[object 自己的构造函数]”

const Fn = function Fn() {};
Fn.prototype[Symbol.toStringTag] = "Fn";
let f = new Fn;
console.log(toString.call(f)); //“[object Fn]”

class Fn {
 [Symbol.toStringTag] = "Fn";
}
let f = new Fn;
console.log(Object.prototype.toString.call(f));//"[object Fn]"

编写工具类utils能实现类型检测

(function() {
"use strict";
/* 检测数据类型的 */
const getProto = Object.getPrototypeOf,
  class2type = {},
  toString = class2type.toString, //Object.prototype.toString
  hasOwn = class2type.hasOwnProperty; //Object.prototype.hasOwnProperty
  //fnToString = hasOwn.toString,//Funtcion.prototype.toStirng
  //ObjectFunctionString = fnToString.call(Object);//'function Object(){native code}'


// 检测是否为函数
const isFunction = function isFunction(obj) {
  return typeof obj === "function" && typeof obj.nodeType !== "number" &&
      typeof obj.item !== "function";
};

// 检测是否为window对象
const isWindow = function isWindow(obj) {
  return obj != null && obj === obj.window;
};

// 通用检测数据类型的办法,返回结果:字符串、含小写的数据类型
const toType = function toType(obj) {
  let reg = /^\[object (.+)\]$/;
  if (obj == null) return obj + "";
  return typeof obj === "object" || typeof obj === "function" ?
      reg.exec(toString.call(obj))[1].toLowerCase() :
      typeof obj;
};

// 检测是否为标准普通对象(纯粹对象)
const isPlainObject = function isPlainObject(obj) {
  let proto, Ctor;
  if (!obj || toString.call(obj) !== "[object Object]") return false;
  proto = getProto(obj);
  if (!proto) return true;
  //proto.hasOwnProperty('constructor') => hasOwn.call(proto, "constructor")
  Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
  //fnToString.call(Ctor) === ObjectFunctionString
  //fnToString.call(Ctor) === fnToString.call(Object)
  return typeof Ctor === "function" && Ctor === Object;
};

// 检测是否为空对象
const isEmptyObject = function isEmptyObject(obj) {
  if (obj == null || !/^(object|function)$/.test(typeof obj)) return false;
  let keys = Object.getOwnPropertyNames(obj);
  if (typeof Symbol !== "undefined") keys = keys.concat(Object.getOwnPropertySymbols(obj));
  return keys.length === 0;
};

// 检测是否为数组或者类数组
const isArrayLike = function isArrayLike(obj) {
  let length = !!obj && "length" in obj && obj.length,
      type = toType(obj);
  if (isFunction(obj) || isWindow(obj)) return false;
  return type === "array" || length === 0 ||
      typeof length === "number" && length > 0 && (length - 1) in obj;
};


/* 暴露API */
const utils = {
  version: '1.0.0',
  isFunction,
  isWindow,
  toType,
  isPlainObject,
  isEmptyObject,
  isArrayLike
};
if (typeof module === "object" && typeof module.exports === "object") module.exports = utils;
if (typeof window !== "undefined") window.utils = utils;
})();

Array.isArray:检测是否为数组

isNaN:检测是否为有效数字


面试题

 alert({
   name: 'xxx'
 }); //=>“[object Object]”  alert会把编写的值转换为字符串宰输出,而对象toString的时候,调用的是Object.prototype.toString这个方法,而这个方法是检测数据类型的

// 真正转换标准普通对象为字符串
//  + JSON.stringify 把对象变为 JSON 格式字符串  ->JSON.parse
//  + Qs.stringify 依托Qs第三方库,我们把对象变为 urlencoded 格式字符串
// 前后端数据通信的时候,我们经常需要把对象变为指定的字符串格式,传递给服务器;或者把服务器返回的指定格式字符串,变为对象!!

let obj = {
name: 'zhufeng',
age: 12,
teacher: 'zhou'
};
// console.log(JSON.stringify(obj)); //'{"name":"zhufeng","age":12,"teacher":"zhou"}'
console.log(Qs.stringify(obj)); //'name=zhufeng&age=12&teacher=zhou' */

3、JS底层存储机制:堆(Heap)、栈(Stack)内存

  • 堆(Heap)、栈(Stack)
  • ECStack(Execution [ˌeksɪˈkjuːʃn] Context Stack)和 EC(Execution Context )
  • GO(Global Object) 和 VO(Varbale Object)

面试题:var a = {n: 1}; var b = a; a.x = a = {n: 2}; console.log(a.x);//undefined console.log(b);//Object

运算符优先级: developer.mozilla.org/zh-CN/docs/…

var a = {n: 1};

var b = a;

a.x = a = {n: 2};

console.log(a.x);//undefined

console.log(b);//Object

Snipaste_2021-07-23_15-14-58.png

知识点:在全局上下文中,首先看VO(G)有没有存在这个变量,如果没有再找GO有没有存在这个对象,如果没有则报错;如果遇到window.xxx,则直接找GO中是否存在,如果不存在则返回undefined,不会报错!

  • a是对象的某个属性,不论a的值是什么类型,a连同他的值都在堆中。

  • a是一个变量

  • a的值是原始值,原始值是存储在栈中的 a和它关联

  • a的值是一个对象,先开辟堆内存,把堆内存的地址放在栈中存储 a和它关联


4、前端必备数据结构:栈结构 & 队列结构(优先级队列)

Snipaste_2021-07-24_15-11-26.png

组织结构 Array

JS中的数组结构非常的简单(已经是浏览器帮助我们进行封装处理好的)

  • 可以存储不同的数据类型值

  • 数组容量伴随存储的内容自动缩放

  • Array.prototype上提供很多供数组操作的方法

优势:基于索引直接进行查找和获取,效率很高

弊端:进行中间插入和删除操作时,性能非常低(数组塌陷和删除中间项的优化)

数组塌陷问题:删除数组中的某一项后,后面每一项的索引都要往前提一位【性能问题】

案例:(删除指定的元素)

let arr = [10, 20, 30, 40, 50];
@1
// let res = arr.splice(1, 1);
// console.log(arr, res);
@2
// arr[1] = arr[arr.length - 1];
// arr.pop();
// console.log(arr);
@3
for (let i = 0; i < arr.length; i++) {
let item = arr[i];
console.log(item);
if (item === 20) {
  arr.splice(i, 1);
  i--;
}
}

栈结构 Stack

后进先出 LIFO(last in first out)

只能在一段操作(顶端front):包括增加(进栈)和删除(出栈)

递归算法中的无限递归会出现栈溢出

代码案例:(后进先出)

class Stack {
container = [];

//进栈
enter(item) {
  return this.container.push(item);
}

//出栈
leave() {
  return this.container.pop();
}

//长度
size() {
  return this.container.length;
}

//内容
value() {
   return this.container.slice(0);
}
}
let sk1 = new Stack;
sk1.enter(10);
sk1.enter(20);
sk1.enter(30);
console.log(sk1.leave());//30

进制

在浏览器中都是以十进制显示

计算机计算是以二进制数进行计算

需求:把十进制数转成二进制数(整数情况下:需要转成二进制的十进制数除以2,将得到的余数拼接起来)

class Stack {
    container = [];

    //进栈
    enter(item) {
        return this.container.push(item);
    }

    //出栈
    leave() {
        return this.container.pop();
    }

    //长度
    size() {
        return this.container.length;
    }

    //内容
    value() {
        return this.container.slice(0);
    }
}

Number.prototype.decimal2binary = function decimal2binary() {
    let decimal = +this;
    //判断是不是整数
    if (/\./.test(decimal)) {
        //包含小数
        throw new RangeError('处理的数字必须是整数');
    }

    //整数
    let sk = new Stack;
    if (decimal === 0) return "0";
    while (decimal > 0) {
        sk.enter(decimal % 2);
        decimal = Math.floor(decimal / 2);
    }
    return sk.value().reverse().join('');
}
let num = 28;
console.log(num.toString(2));
console.log(num.decimal2binary());

补充:

  • new Number(10); 返回的结果为 Number {10},是一个对象。

  • +new Number(10); 返回的结果为 10,才是一个数字。

  • 如何判断一个数是否为整数?

    先转为字符串,在判断是否含有小数点(可使用正则/\.test(number)/)

队列结构 Queue

先进先出 FIFO (first in first out)

允许在前端(front)删除,允许在后端(rear)插入

特殊:优先级队列

面试题:击鼓传花(N个人一起玩游戏,围成一圈,从1开始数数,数到M的人自动淘汰;最后剩下的人会取得胜利,问最后剩下的是原来的哪一位?)

//队列结构

class Queue {
    container = [];

    // 从顶端进入队列
    enter(item) {
            this.container.unshift(item);
        }
        // 从底端出队列
    leave() {
            return this.container.pop();
        }
        // 长度
    size() {
            return this.container.length;
        }
        // 内容
    value() {
        return this.container.slice(0);
    }
}

//击鼓传花
// + n参与的人数 >= 1
// + m被淘汰的数字
const game = function game(n, m) {
    let qe = new Queue;
    //让参与的人一次进入这个队列
    for (let i = 1; i <= n; i++) {
        qe.enter(i);
    }

    while (qe.size() > 1) {
        //只要的队列中还存在两个及两个以上的人,则重复这个循环
        for (let i = 1; i < m; i++) {
            qe.enter(qe.leave());
        }
        //数到m的人直接出队列被淘汰
        qe.leave();
    }
    return qe.value()[0];
}
console.log(game(8, 5));

补充:

  • 击鼓传花的循环要从1开始。