前端学习记录-js篇

135 阅读15分钟

为了督促自己学习,每天记录自己学习的相关,就是学习的总结和相关的一些知识点记忆

1.JavaScript相关

1. JavaScript有哪些数据类型,它们的区别?

JavaScript共有八种数据类型,分别是Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt

其中 Symbol 和 BigInt 是ES6 中新增的数据类型:

Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。 BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

这些数据可以分为原始数据类型和引用数据类型: 栈:原始数据类型,也叫基本数据类型(Undefined、Null、Boolean、Number、String、Symbol、BigInt) 堆:引用数据类型,也叫复杂数据类型(对象、数组和函数)

两种类型的区别:

  1. 从内存角度来说,原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。获取数据是直接获取。 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。获取数据是间接获取。

  2. 从赋值角度来说,基本数据类型赋的是值,引用数据类型赋的是地址。

  3. 从函数传参角度来说,基本数据类型传的是值,引用数据类型传的是地址。

堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:

在数据结构中,栈中数据的存取方式为先进后出。 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。

在操作系统中,内存被分为栈区和堆区:

栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。

2. 数据类型检测的方式有哪些?

(1)typeof 其中数组、对象、null都会被判断为object,其他判断都原样返回。

(2)instanceof instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

(3) constructor

console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true

constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:

function Fn(){};
 
Fn.prototype = new Array();
 
var f = new Fn();
 
console.log(f.constructor===Fn);    // false
console.log(f.constructor===Array); // true

(4)Object.prototype.toString.call()

Object.prototype.toString.call() 使用 Object 对象的原型方法 toString 来判断数据类型: 同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?

这是因为toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。

3.判断数组是不是数组(除上面意外的方法)?

  • 通过原型链做判断 obj.__proto__ === Array.prototype;

  • 通过ES6的Array.isArray()做判断 Array.isArrray(obj);

  • 通过Array.prototype.isPrototypeOf 从原型入手: Array.prototype.isPrototypeOf(obj)

4. null和undefined区别

首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。

undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。

undefined 在 JavaScript 中不是一个保留字,这意味着可以使用 undefined 来作为一个变量名,但是这样的做法是非常危险的,它会影响对 undefined 值的判断。我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。

当对这两种类型使用 typeof 进行判断时,Null 类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

5.isNaN 和 Number.isNaN 函数的区别?

isNaN()是ES5的方法,Number.isNaN()是ES6的方法.

函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

  1. == 操作符的强制类型转换规则? 对于 == 来说,如果对比双方的类型不一样,就会进行类型转换。假如对比 x 和 y 是否相同,就会进行如下判断流程:
  • 首先会判断两者类型是否相同,相同的话就比较两者的大小;类型不相同的话,就会进行类型转换;
  • 会先判断是否在对比 null 和 undefined,是的话就会返回 true
  • 判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number
  • 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
  • 判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类再判断 '1' == { name: 'js' } ↓ '1' == '[object Object]'

image.png

7.Object.is() 与比较操作符 “=”、“” 的区别?

  • 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
  • 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。
  • 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

8.JavaScript 中如何进行隐式类型转换?

image.png

布尔操作符(!、&&、||)

当使用 条件判断语句(if...else)  以及 布尔操作符(!、&&、||)  时,会调用Boolean()进行隐式类型转换

  • 转换为false的有:0, null, undefined, NaN, '', false
  • 其余都为true([], {}为true)
乘/除法操作符(*、/)减法操作符(-)
  • 若有一个操作数为NaN,则结果为NaN
  • 若其中一个操作数不为数字,则调用Number()将其转换为数值
加法操作符(+)

+操作符+操作符的两边有至少一个string类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。若有一个操作数为NaN,则结果为NaN.

关系操作符(>、<、>=、<=)
  • 与NaN比较都会返回false
  • 若两个操作数都为字符串,则比较字符串的编码值
  • 若有一个操作数为数值,则对另一个操作数用Number()转换
  • 若有一个操作数为对象,则调用该对象valueOf(),没有valueOf()则调用toString()
相等操作符(==、!=)
  • 字符串、布尔类型和数值比较,现将前者用Number()转换为数值
  • 若一个操作数是对象另一个不是,则调用该对象valueOf(),没有valueOf()则调用toString()
  • 若两个操作数都是对象,则比较它们是不是同一个对象(地址是否相同)
  • null和undefined是相等的
  • null和undefined不会转换成任何值
  • 任何数都不等于NaN,包括NaN自己

复杂数据类型的转换规则:

1.先使用valueOf()方法获取原始值
2.再使用toString()转成字符串
3.在使用number把字符串转成数字

console.log([1,2].valueOf())    //[1,2]
console.log([1,2].valueOf().toString())   //1,2

console.log({name:"test"}.valueOf())   //{name:”test“}
console.log({name:"test"}.valueOf().toString())  //[object Object]

console.log([].valueOf())    //[]
console.log([].valueOf().toString())   //""

console.log({}.valueOf())   //{}
console.log({}.valueOf().toString())  //[object Object]

  `{} == 0`会报错,是因为在浏览器的控制台上直接这样写的话,`{}`被**当作空代码块**,而不是对象,最后执行实际上是 `== 0`,如果你写`({} == 0)`则为`false`

9. let、const、var的区别

(1)块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题: 内层变量可能覆盖外层变量; 用来计数的循环变量泄露为全局变量 (2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否则会报错。

(3)给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。

(4)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。

(5)暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。

(6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。

(7)指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。基本数据类型不能再赋值.

10.如果new一个箭头函数的会怎么样

箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this指向,更不可以使用arguments参数,所以不能New一个箭头函数。

new操作符的实现步骤如下:

  1. 创建一个对象
  2. 将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性)
  3. 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法)
  4. 返回新的对象 所以,上面的第二、三步,箭头函数都是没有办法执行的。

11.箭头函数与普通函数的区别

(1)箭头函数比普通函数更加简洁

如果没有参数,就直接写一个空括号即可; 如果只有一个参数,可以省去参数的括号; 如果有多个参数,用逗号分割; 如果函数体的返回值只有一句,可以省略大括号; 如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常见的就是调用一个函数: let fn = () => void doesNotReturn();

(2)箭头函数没有自己的this 箭头函数不会创建自己的this, 所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变

(3)箭头函数继承来的this指向永远不会改变

对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。需要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。

(4)call()、apply()、bind()等方法不能改变箭头函数中this的指向

(5)箭头函数不能作为构造函数使用

构造函数在new的步骤在上面已经说过了,实际上第二步就是将函数中的this指向该对象。 但是由于箭头函数时没有自己的this的,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。

(6)箭头函数没有自己的arguments

箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。

(7)箭头函数没有prototype

(8)箭头函数不能用作Generator函数,不能使用yeild关键字

2. 关于js手写代码相关

交换两个变量的值

第一种:重新添加一个变量

let t;
t = a;
a = b;
b = t;

第二种:利用加法

 a = a + b; 
 b = a - b; 
 a = a - b;

第三种:利用解构赋值. ES6 允许写成下面这样。

let [a, b, c] = [1, 2, 3];

上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。所以我们可以写成下面这样:

[a,b] = [b,a];

第四种:赋值给一个数组

a = [a,b];

b = a[0];

a = a[1];

第五种:赋值给一个对象

a = {a:b,b:a};

b = a.b;

a = a.a;

第六种:一行代码解决问题

a = [b,b=a][0];

实现数组的乱序输出

主要的实现思路就是:

  • 取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行交换。
  • 第二次取出数据数组第二个元素,随机产生一个除了索引为1的之外的索引值,并将第二个元素与该索引值对应的元素进行交换
  • 按照上面的规律执行,直到遍历完成
var arr = [1,2,3,4,5,6,7,8,9,10]; 
for (var i = 0; i < arr.length; i++) { 
const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;
//Math.random()返回介于 0(包含) ~ 1(不包含) 之间的一个随机数: 
//Math.round() 方法可把一个数字舍入为最接近的整数;
//如果传入负数,小数点第一位是5时,直接舍去,整数部分不 +1;
//传入的是正数,小数点第一位是5时,往整数部分 +1;
[arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];
//解构赋值,交换变量的位置上
} 
console.log(arr)

还有一方法就是倒序遍历:

var arr = [1,2,3,4,5,6,7,8,9,10];
let length = arr.length, randomIndex, temp;
while (length) { 
randomIndex = Math.floor(Math.random() * length--); 
//Math.ceil()  “向上取整”, 即小数部分直接舍去,并向正数部分进1;
//Math.floor()  “向下取整” ,即小数部分直接舍去
temp = arr[length]; 
arr[length] = arr[randomIndex]; 
arr[randomIndex] = temp; 
//交换变量位置 参考第一个问题记录
} 
console.log(arr)

实现数组元素求和

数组reduce()这个方法接收两个参数:

  • 要执行的函数,必需.要执行的函数中也可传入参数,分别为
    • prev:初始值或者上次调用函数的返回值(必需)
    • cur:当前元素(必需)
    • index:当前元素的索引
    • arr:被遍历的数组
  • 函数迭代的初始值(可选)
let arr=[1,2,3,4,5,6,7,8,9,10];
let sum = arr.reduce( (total,i) => total += i,0);


var = arr=[1,2,3,[[4,5],6],7,8,9] 
//先扁平化处理然后再累加
let arr= arr.toString().split(',').reduce( (total,i) => total += Number(i),0);

递归实现:

let arr = [1, 2, 3, 4, 5, 6] ;
function add(arr) { 
if (arr.length == 1) return arr[0] ; //只剩最后一项了,退出循环
return arr[0] + add(arr.slice(1))
} 
console.log(add(arr)) // 21

实现数组的扁平化

数组的扁平化,就是将一个嵌套多层的数组 array (嵌套可以是任何层数)转换为只有一层的数组。

(1)递归实现

普通的递归思路很容易理解,就是通过循环递归的方式,一项一项地去遍历,如果每一项还是一个数组,那么就继续往下遍历,利用递归程序的方法,来实现数组的每一项的连接:

let arr = [1, [2, [3, 4, 5]]]; 
function flatten(arr) { 
let result = []; 
for(let i = 0; i < arr.length; i++) {
if(Array.isArray(arr[i])) { 
   result = result.concat(flatten(arr[i])); 
} else { 
   result.push(arr[i]);
 }
}
return result; 
} 
flatten(arr); // [1, 2, 3, 4,5]

(2)reduce 函数迭代

从上面普通的递归函数中可以看出,其实就是对数组的每一项进行处理,那么其实也可以用reduce 来实现数组的拼接,从而简化第一种方法的代码,改造后的代码如下所示:

let arr = [1, [2, [3, 4]]];
function flatten(arr) { 
 return arr.reduce(function(prev, next){ 
   return prev.concat(Array.isArray(next) ? flatten(next) : next) }, [])
   } 
console.log(flatten(arr));// [1, 2, 3, 4,5]

(3)扩展运算符实现

这个方法的实现,采用了扩展运算符和 some 的方法,两者共同使用,达到数组扁平化的目的:

let arr = [1, [2, [3, 4]]]; 
function flatten(arr) { 
 while (arr.some(item => Array.isArray(item))) {
 arr = [].concat(...arr); } 
return arr; }
//some() 方法用于检测数组中的元素是否满足指定条件(函数提供)。 
//some() 方法会依次执行数组的每个元素:如果有一个元素满足条件,则表达式返回true, 剩余的元素不会再执行检测。如果没有满足条件的元素,则返回false。 
//注意: some() 不会对空数组进行检测。 
//注意: some() 不会改变原始数组。 
console.log(flatten(arr)); // [1, 2, 3, 4,5]

(4)split 和 toString

可以通过 split 和 toString 两个方法来共同实现数组扁平化,由于数组会默认带一个 toString 的方法,所以可以把数组直接转换成逗号分隔的字符串,然后再用 split 方法把字符串重新转换为数组,如下面的代码所示:

let arr = [1, [2, [3, 4]]]; 
function flatten(arr) { 
  return arr.toString().split(','); 
} 
console.log(flatten(arr)); // [1, 2, 3, 4,5]

通过这两个方法可以将多维数组直接转换成逗号连接的字符串,然后再重新分隔成数组。

(5)ES6 中的 flat

我们还可以直接调用 ES6 中的 flat 方法来实现数组扁平化。flat 方法的语法:arr.flat([depth])

其中 depth 是 flat 的参数,depth 是可以传递数组的展开深度(默认不填、数值是 1),即展开一层数组。如果层数不确定,参数可以传进 Infinity,代表不论多少层都要展开:

let arr = [1, [2, [3, 4]]]; 
function flatten(arr) { 
  return arr.flat(Infinity); 
} 
console.log(flatten(arr)); // [1, 2, 3, 4,5]
//不传参
var array1 = [1, 2, [3, 4], [[5, 6]], [[[7, 8]]], [[[[9, 10]]]]];\
var array2 = array1.flat();\
// array2: [1, 2, 3, 4, [5, 6], [[7, 8]], [[[9, 10]]]]

//调用 flat() 函数时不带参数值。
//考虑到可选参数的默认值,此函数调用与 flat(1) 相同。这意味着原始数组中深度为 1 的任何数组都将被完全展平,以便将其所有内容单独连接到新数组。

传参的情况下
var array1 = [1, 2, [3, 4], [[5, 6]], [[[7, 8]]], [[[[9, 10]]]]];
var array2 = array1.flat(Infinity);
// array2: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

可以看出,一个嵌套了两层的数组,通过将 flat 方法的参数设置为 Infinity,达到了我们预期的效果。其实同样也可以设置成 2,也能实现这样的效果。在编程过程中,如果数组的嵌套层数不确定,最好直接使用 Infinity,可以达到扁平化。

(6)正则和 JSON 方法

在第4种方法中已经使用 toString 方法,其中仍然采用了将 JSON.stringify 的方法先转换为字符串,然后通过正则表达式过滤掉字符串中的数组的方括号,最后再利用 JSON.parse 把它转换成数组:

let arr = [1, [2, [3, [4, 5]]], 6]; 
function flatten(arr) { 
let str = JSON.stringify(arr); 
//转换后[1,[2,[3,[4,5]]],6]
str = str.replace(/(\[|\])/g, ''); 
//转换后 1,2,3,4,5,6
str = '[' + str + ']'; 
return JSON.parse(str);
} 
console.log(flatten(arr)); // [1, 2, 3, 4,5]

实现数组的flat方法

function _flat(arr, depth) {
 if(!Array.isArray(arr) || depth <= 0) {
   return arr;
 }
 return arr.reduce((prev, cur) => {
   if (Array.isArray(cur)) {
   //通过递归实现
     return prev.concat(_flat(cur, depth - 1))
   } else {
     return prev.concat(cur);
   }
 }, []);
}

手动实现数组push方法

push() 方法可向数组的末尾添加一个或多个元素,并返回新的长度。

let arr = [];
Array.prototype.push = function() {
    for( let i = 0 ; i < arguments.length ; i++){
        this[this.length] = arguments[i] ;
    }
    return this.length;
}

实现数组去重

给定某无序数组,要求去除数组中的重复数字并且返回新的无重复数组。

ES6方法(使用数据结构集合):

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
//注意:Set是es6新增的数据结构,类似于数组,但它的一大特性就是所有元素都是唯一的,没有重复的值,我们一般称为集合。
(1) 
//new Set后返回的是 Set(6) {1, 2, 3, 5, 9, 8} 再解构赋值
let unique = [...new Set(array) ]; 
(2)
//Array.from()方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组。所谓类数组对象,最基本的要求就是具有length属性的对象。允许在 `JavaScript` 集合(如: 数组、类数组对象、或者是字符串、`map` 、`set` 等可迭代对象) 上进行有用的转换。
Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]

拓展--字符串去重

let str = "352255"; 
let unique = [...new Set(str)].join(""); // 352

ES5方法:使用map存储不重复的数字

const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];

uniqueArray(array); // [1, 2, 3, 5, 9, 8]

function uniqueArray(array) {
  let map = {};
  let res = [];
  for(var i = 0; i < array.length; i++) {
  //hasOwnProperty()方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。
  //没有就往对象和数字里面推
    if(!map.hasOwnProperty([array[i]])) {
      map[array[i]] = 1;
      res.push(array[i]);
    }
  }
  return res;
}

实现数组的filter方法

filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。

语法是array.filter(function(currentValue, index, arr), thisValue)

  • currentValue 必填项 当前元素的值
  • index 当前元素索引值
  • arr 原数组 当然一般由currentValue这个较少用到
  • thisValue 可选 较少用到 官方说法:对象作为该执行回调时使用,传递给函数,用作 “this” 的值。 如果省略了 thisValue,或者传入 null、undefined,那么回调函数的 this 为全局对象。

注意:filter() 不会对空数组进行检测。filter() 不会改变原始数组。

Array.prototype._filter = function(fn) {
    if (typeof fn !== "function") {
        throw Error('参数必须是一个函数');
    }
    const res = [];
    for (let i = 0, len = this.length; i < len; i++) {
        fn(this[i]) && res.push(this[i]);
    }
    return res;
    }