# JavaScript

# 关于JS

# JS是一门怎样的语言

JS是一门面向对象、弱类型、跨平台的动态脚本语言。

# V8执行JS的过程

# 生成AST(抽象语法树)

# 词法分析

词法分析即分词,它的工作就是将一行行的代码分解成一个个token。

let name = 'fendy'

image-20200318151207885

# 语法分析

将生成的这些 token 数据,根据一定的语法规则转化为AST。

# 将AST转成字节码

# 执行代码

由解释器逐行执行字节码,遇到热点代码启动编译器进行编译,生成对应的机器码, 以优化执行效率。

# JS引擎解析过程

# JS存储数据的方式

1、简单数据类型以的形式存储,存储的是值。

2、复杂数据类型以的形式存储,地址(指向堆中的值)存储在中。

# 变量

image-20210913112008760

暂、变、作、初、重

# 初始化值

# 重复声明

# 暂时性死区

在块级作用域里面,如果寻找不到变量,它不会去上一级作用域继续寻找作用域了。

var tmp = 123;

if (true) {
    tmp = 'abc'; //ReferenceError: Cannot access 'tmp' before initialization
    let tmp;
}

# 变量提升

console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
let a=1;

console.log(c); // undefined
var c=1    

# 作用域

var : 全局、函数

let :全局、函数、块级

const:全局、函数、块级

# 作用域与内存

# 作用域

作用域就是变量和函数的可访问范围。它规定了如何查找变量,也就是当前执行代码对变量的访问权限。

**执行上下文:**可以简单理解为一个对象。它有三种类型,全局执行上下文、函数执行上下文、eval()执行上下文。

**分类:**全局作用域、函数作用域、块级作用域(ES6)

# 作用域链

一般情况下,变量取值到 创建 这个变量 的函数的作用域中取值。但是如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。

**作用:**保证当前执行环境里,有权访问的变量和函数是有序的(作用域链的变量只能向上访问变量,访问到window对象时被终止)

作用域链和原型继承查找的区别: 如果去查找一个普通对象的属性,但是在当前对象和其原型中都找不到时,会返回undefined;但查找的属性在作用域链中不存在的话就会抛出ReferenceError

# this

# 箭头函数

  1. 在箭头函数中,this 引用的是定义箭头函数的上下文。
  2. 事件回调中,使用箭头函数取代_this=this重复定义上下文的问题。

# 绑定方式

绑定方式优先级:new > 显式绑定 > 隐式绑定> 直接调用

# new绑定

此时构造函数中的this指向实例对象

# 显式绑定

call、apply、bind区别:

1、call()、apply()属于立即执行函数,区别在于接收的参数形式不同,前者是一次传入参数,后者参数可以是数组。

2、bind()则是创建一个新的包装函数,并且返回,它不会立即执行bind(this,arg1,arg2···)。

3、在严格模式下,调用函数时如果没有指定上下文对象,则this值不会指向window。 除非使用 apply()或 call()把函数指定给一个对象,否则 this 的值会变成 undefined。

function foo(){
    console.log(this.a);
    bar.apply({a:5},arguments)
}
function bar(b){
    console.log(this.a + b);
}

var a = 1; // 全局 a 变量
foo(3)
# 隐式绑定

对象调用,指向对象

function foo(){
    console.log(this.a)
}
var obj = {
    a = 1,
    foo
}
obj.foo()    // 1 → this 指向 obj
# 默认绑定

全局调用,this指向window

function foo(){
    console.log(this.a)
}
var a = 2
foo() // 2 → this指向全局
# DOM事件绑定

元素监听事件addEventerListener中 this 默认指向绑定事件的元素。

# 箭头函数

箭头函数不能使用 arguments、super 和 new.target,也不能用作构造函数。此外,箭头函数也没有 prototype 属性。

  1. 没有arguments
  2. 没有prototype
  3. 没有super
  4. 没有new.target
  5. 没有属于自己的this
  6. 不能作为构造函数

# 原型&原型链

# 原型

原型是一个简单的对象,用于实现对象的属性继承。 每一个JavaScript对象(null除外)在创建的时候就会有一个与之关联的对象,这个对象就是原型对象,每一个对象都会从原型上继承属性。

# 构造函数

可以通过new来创建一个对象的函数。

# 实例

通过构造函数和new创建出来的对象。

# 三者关系

image-20210909110730173

# 获取原型方法

假设person是一个实例

  1. person.constructor.prototype
  2. person._ proto __
  3. Object.getPrototypeOf(person) = person._ proto __

# 原型链

JavaScript对象通过prototype指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条, 即原型链。

# 原型链机制

  1. 属性查找机制

    当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象Object.prototypeObject.prototype.__proto__ === null),假如还是没有找到,则输出undefined。

  2. 属性修改机制

    只会修改实例对象本身的属性,如果不存在,则进行添加该属性。如果需要修改原型的属性,则需要通过prototype属性(b.prototype.B = 1),但这样修改会导致所有继承于这个对象的实例的属性发生改变。

# 查找属性方法

  1. hasOwnProperty() // 不查找原型上面的属性
  2. 使用 in 检查对象中是否含有某个属性 // 遍历所有的key,包括原型上的

# 闭包

# 定义

闭包是指有权访问另外一个函数作用域中变量的函数。 简单来说,闭包就是一个函数A内部有另一个函数B,函数B可以访问到函数A的变量方法,此时函数B就是闭包。

# 产生原因

当前环境中存在指向父级作用域的引用。

# 特征

  1. 闭包可以更新外部变量的值。
  2. 内部函数可以引用外层的参数和变量。
  3. 参数和变量不会被垃圾回收制回收。

# 优点

  1. 能够封装对象的私有属性和私有方法。
  2. 可以读取函数内部的变量,并且让这些变量始终保持在内存中。

# 缺点

闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。

解决方法:在退出函数之前,将不使用的局部变量全部删除

# 具体应用场景

# 函数防抖节流

# 使用闭包设计单例模式

class CreateUser {
  constructor (name) {
    this.name = name
    this.getName()
  }

  getName () {
    return this.name
  }
}

// 代理实现单例模式
const ProxyMode = (function () {
  let instance = null
  return function (name) {
    if (!instance) {
      instance = new CreateUser(name)
    }
    return instance
  }
})()

# 封装使用变量或者私有属性

# 经典问题

for(var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
//结果为 3 3 3


//利用IIFE(立即执行函数表达式)构建闭包解决var没有块级作用域的问题
for(var i = 1;i < 3; i++){
    (function (i) {
        setTimeout(()=>{
            console.log(i);
        })
    })(i)
}
//结果为 0 1 2 


for(let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
// 结果为 0 1 2 
// 每个 let 和代码块结合起来形成块级作用域,当 setTimeout() 打印时,会寻找最近的块级作用域。


//给定时器传入第三个参数, 作为timer函数的第一个函数参数
for (var i = 0; i < 5; i++) {
    setTimeout(function (j) {
        console.log(j);
    }, 0, i);
}

# 数据类型

7种基本数据类型1种引用数据类型

# 基本数据类型

Number、Null、Undefined、String、Boolean、Symbol、BigInt

(NNUSB)

BigInt: BigInt是一种新的数据类型,用于当数值大于Number数据类型的支持。

# 引用数据类型

Object,Array和Function属于Object

# Object

# Array

# Function

# 数据类型转换

# 数字取舍

# 四舍五入

Math.round(2.33) // 3

# 向上取整
  1. Math.ceil(2.33) // 3
  2. 2.33|1
# 向下取整
  1. Math.floor(2.33);
  2. 2.33|0

# ==的转换规则

  1. 两边的类型是否相同,相同的话就比较值的大小,例如1==2,返回false
  2. 两边如果是null和undefined,就返回true。null、undefined和其他任何值比较均为false
  3. 两边是String和Number,String转成Number
  4. NaN和任何相比都返回false(包括自己)
  5. 一边为Object另一边为String、Number、Symbol,Object会转成字符串

# 对象转原始类型流程

  1. 如果Symbol.toPrimitive()方法,优先调用再返回。
  2. 调用valueOf(),如果转换为原始类型,则返回。//valueOf 返回 String 对象的原始值
  3. 调用toString(),如果转换为原始类型,则返回
  4. 如果都没有返回原始类型,会报错。
let person={ name:'Fendy',age: 16 }
person.toString() // '[object Object]'
person.toString=()=>{ return 'toString' }
person.toString()    // 'toString'
person == 'toString' // true

person.valueOf=()=>{ return 'valueOf' }
person == 'toString' // false
person == 'valueOf' // true

# 加法运算

当运算符其中一方为字符串时,那么另一方也转换为字符串

# 进制转换

# 十进制转其它进制

(10).toString(2)    // 十进制二进制

# 其它进制转十进制

parseInt('1010',2)    // 10

# 判断数据类型

# typeof

  1. 能够正确判断简单数据类型(原始类型),除了null,typeof null结果为object。
  2. typeof不能正确判断对象类型,typeof仅可以区分开function,除此之外,结果均为object。

之所以typeof null 会输出 object,这是JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object 。

# instanceof

# 作用

检测某个构造函数的原型对象在不在某个对象的原型链上,能够准确判断复杂数据类型,但是不能正确判断简单数据类型。

# 原理

instanceof是通过原型链进行判断的,A instanceof B,在A的原型链中层层查找,查找是否有原型等于B.prototype,如果一直找到A的原型链的顶端,即Object.prototype.__proto__,仍然不等于B.prototype,那么返回false,否则返回true

# 手写instanceof
function myInstanceof(left,right) {
    // 如果是基本数据类型,直接返回false
    if (left==null || typeof left!=='object') return false;
    //getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
    let proto = Object.getPrototypeOf(left);
    while (true) {
        if (proto==null) return false;
        if (proto===right.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
}

console.log(myInstanceof(111, Number));    //false
console.log(myInstanceof(new Number(111), Number));    //true

# Object.prototype.toString.call

//判断是否为数组
Object.prototype.toString.call(arr) === "[object Array]"

// 判断对象
Object.prototype.toString.call(person) === '[object Object]'

# Array

# 判断数组

  1. Array.isArray(obj ) // ES6 api
  2. obj instanceof Array // 原型链查找
  3. obj.constructor === Array // 构造函数类型判断
  4. Object.prototype.toString.call(arr) === "[object Array]" toString 返回表示该对象的字符串,若这个方法没有被覆盖,那么默认返回 "[object type]" ,其中 type 是对象的类型。需要准确判断类型的话,建议使用这种方法。

# 类数组转数组

  1. Array.from(arr)
  2. [...arr]

# 相关方法

/*1、map
作用: 遍历数组,返回回调返回值组成的新数组。
参数:(value, index, array) => {}
返回值:新数组。
*/
let arr = [1, 2, 3, 4, 5];
let b = arr.map((i) => {
    return i + 2;
});
console.log(b)//b:[3,4,5,6,7]

/*2、forEach
作用: 调用数组的每个元素,并将元素传递给回调函数。
参数:(item,index,arr)=>{}
返回值:undefined。
forEach无法break,可以用try/catch中throw new Error来停止。
forEach() 对于空数组是不会执行回调函数的。
*/
let arr = [1, 2, 3, 4, 5, 5, 1];
arr.forEach((value, index, array) => {
    array[index] = value * 5;
});
console.log(arr);    //[5, 10, 15, 20, 25, 25, 5]

/*3、filter
作用:检测数值元素,并返回符合条件所有元素的数组。
参数:(value) => {}
返回值:新数组
filter() 不会对空数组进行检测。
filter() 不会改变原始数组。
*/
let arr = [1, 2, 3, 4, 5, 5, 1];
let b = arr.filter((value) => value > 3);
console.log(b);    // [4, 5, 5]



/*4、some
作用:方法用于检测数组中的元素是否满足指定条件。有一个元素满足条件,则表达式返回true
参数:(value, index, array)=>{}
返回值:boolean
some() 不会对空数组进行检测。
*/
let arr = [1, 2, 3, 4, 5, 5, 1];
let b = arr.some((value, index, array) => value > 6);
console.log(b);    // false

/*5、every
作用:检测每个元素是否都符合条件。有一个元素不满足,则表达式返回false
参数:(value, index, array)=>{}
返回值:boolean
every() 不会对空数组进行检测。
*/
let arr = [1, 2, 3, 4, 5, 5, 1];
let b = arr.every((value, index, array) => value > 0);
console.log(b);    // true



/*6、join
作用:把数组中的所有元素转换一个字符串。
参数:separator。指定要使用的分隔符。如果省略该参数,则使用逗号作为分隔符。
返回值:string
*/
let arr = [1, 2, 3, 4, 5, 5, 1];
let b = arr.join("_");
console.log(b);    // 1_2_3_4_5_5_1

/*7、concat
作用:连接两个或多个数组。
参数:array2,array3,...,arrayX
返回值:新数组
*/
let [animal,fruit,country] =[ ['cat', 'dog'],['apple', 'pear'], ['China', 'USA']];
let mix = animal.concat(fruit, country);
console.log(mix);    // ["cat", "dog", "apple", "pear", "China", "USA"]

//合并数组,改变数组arr,返回数组长度。
[].push.allay(arr,arr1)



/*8、push/unshift
作用:在尾部或头部添加元素
参数:item1, item2, ..., itemX
返回值:number,新数组长度
改变原数组。
*/
let animal = ['cat', 'dog'];
let animal2 = animal.push('bird');
console.log(animal);    // ["cat", "dog", "bird"]
console.log(animal2);    // 3



/*9、pop/shift
作用:删除数组最后一个或第一个元素
参数:无
返回值:返回被删除的元素
改变原数组。
*/
let animal = ['cat', 'dog'];
let animal2 = animal.shift();
console.log(animal);    // ["dog"]
console.log(animal2);    // cat



/*10、slice
作用:截取选定区域的数组元素。
参数:start, end    0指第一个元素,-1指最后一个元素。
返回值:截取元素组成的新数组
*/
let animal = ['cat', 'dog', 'bird', 'duck', 'pig'];
let res = animal.slice(0, 2);
console.log(animal);    // ["cat", "dog", "bird", "duck", "pig"]
console.log(res);    // ["cat", "dog"]

/*11、splice
作用:添加或删除数组中的元素。
参数:index,howmany,item1,.....,itemX    index:添加/删除元素的索引。howmany:删除多少元素,默认删除index到最后。item:添加的新元素。
返回值:删除元素组成的新数组
改变原数组。
*/
let animal = ['cat', 'dog', 'bird', 'duck', 'pig'];
let res = animal.splice(-2, 2, 'fox');
console.log(animal);    // ["cat", "dog", "bird", "fox"]
console.log(res);    // ["duck", "pig"]

//指定位置插入指定元素
function insert(arr, item, index) {
    return arr.slice(0,index).concat(item,arr.slice(index));
}


/*12、indexOf/lastIndexOf
作用:查找元素第一次/最后一次出现的位置
参数:item,start    如果找到一个item,则返回item的第一次出现的位置,没找到指定元素则返回-1。
返回值:number
*/
let animal = ['cat', 'dog', 'bird', 'duck', 'pig'];
let res = animal.indexOf('bird');
console.log(res);    // 2



/*13、reduce/reduceRight
作用:接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。reduceRight从末尾到开头累加。
参数:((previousValue, currentValue, currentIndex, array) => {})
返回值:累加的结果
对于空数组是不会执行回调函数的,只有初始值直接返回初始值。
*/
let animal = ['cat', 'dog', 'bird', 'duck', 'pig'];
let res = animal.reduce(((previousValue, currentValue, currentIndex, array) => previousValue +"-"+ currentValue));
console.log(res);    // cat-dog-bird-duck-pig

let sort = [10, 20, 30, 40, 50];
let res = sort.reduce((previousValue, currentValue) => {
    return previousValue + currentValue;
});
console.log(res);    //    150



/*14、sort
作用:对数组里的字符串或数字进行排序。
参数:(a, b) => {}。默认按字母类型升序,
返回值:排序后的数组
改变原始数组
*/
let animal = ['cat', 'dog', 'bird', 'duck', 'pig'];
let numbers = [66, 20, 9, 7, 50];
console.log(numbers.sort());    // [20, 50, 66, 7, 9]
console.log(animal.sort());        // ["bird", "cat", "dog", "duck", "pig"]

console.log(numbers.sort(((a, b) => a - b)));// [7, 9, 20, 50, 66]
console.log(animal.sort((a, b) => a - b));// ["bird", "cat", "dog", "duck", "pig"]

console.log(numbers.sort(((a, b) => b - a))); // [66, 50, 20, 9, 7]
console.log(animal.sort((a, b) => b - a)); // ["bird", "cat", "dog", "duck", "pig"]



/*15、reverse
作用:反转数组
参数:无
返回值:反转后的新数组
改变原始数组
*/
let animal = ['cat', 'dog', 'bird', 'duck', 'pig'];
let res = animal.reverse();
console.log(res);    // [ 'pig', 'duck', 'bird', 'dog', 'cat' ]



/*16、flat()
作用:扁平化数组
参数:无
返回值:新数组
*/
let arr = [1, 2, [3, 4], 5, 6];
let res=arr.flat()
console.log(res);     // [1, 2, 3, 4, 5, 6]



/*16、find()
作用:返回数组中满足条件的第一个元素的值,如果没有,返回undefined
参数:
返回值:返回数组中满足条件的第一个元素的值
*/
let arr=[1,2,3,4];
let result = arr.find(item =>{
    return item > 3
});
console.log(result);    //4

/*17、findeIndex()
作用:返回数组中满足条件的第一个元素的下标,如果没有返回-1
参数:
返回值:返回数组中满足条件的第一个元素的下标
*/
let arr=[1,2,3,4];
let result = arr.findeIndex(item =>{
    return item > 3
});
console.log(result);    //3

# Object

# 相关方法

# Object.keys()

# Object.values()

# Object.entries()

# Object.fromEntries()

# String对象

# 属性

# 方法

# charAt

/*
作用:返回指定位置的字符
参数:无
返回值:字符
*/
let str = 'IamFendy';
let ans = str.charAt(3);
console.log(ans);    // F

# indexOf/lastIndexOf

/*
作用:查找字符串首次出现的位置/从后向前搜索字符串,并从起始位置(0)开始计算返回字符串最后出现的位置。
参数:字符串
返回值:number
注意:1、找不到就返回-1
         2、区分大小写
*/
let str = 'I am fendy,';
let ans = str.indexOf('fendy');
console.log(ans);    // 5

# includes

/*
作用:判断字符串是否包含指定的子字符串
参数:字符串
返回值:boolean
*/
let str = 'I am fendy,';
let ans = str.includes('fendy');
console.log(ans);    // true
/*
作用:查找相匹配的字符串
参数:(searchvalue)
返回值:首次匹配到的起始位置,找不到返回-1
*/
let str="I love Fendy";
let ans=str.search('Fendy');
console.log(ans);    // 7

# match

/*
作用:在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。
参数:(regexp)
返回值:存放匹配结果的数组。该数组的内容依赖于 regexp 是否具有全局标志 g。 如果没找到匹配结果返回 null 。
*/
let str="The rain in SPAIN stays mainly in the plain";
let ans=str.match(/ai/ig);
console.log(ans);    // ["ai", "AI", "ai", "ai"]

# concat

/*
作用:拼接字符串
参数:字符串
返回值:拼接后的字符串
*/
let str = 'I am fendy,';
let str1 = 'str1',str2='str2';
let ans = str.concat(str1,str2);
console.log(ans);    // I am fendy,str1str2str2

# repeat

/*
作用:字符串复制指定次数
参数:(count)
返回值:返回复制指定次数并连接在一起的字符串。
*/
let str="I love Fendy";
let ans=str.repeat(2);
console.log(ans);    // 'I love FendyI love Fendy'

# slice/substring

/*
作用:截取字符串
参数:start(包含) 和 end(不包含)
返回值:被截取的字符串
注意:-1 指字符串的最后一个字符,-2 指倒数第二个字符 (substring()不支持从后面截取)
*/
let str = 'I am fendy,';
let ans = str.slice(2, 4);
console.log(ans);    // am

# substr

/*
作用:截取指定数目的字符
参数:start(包含) 和 n(长度)
返回值:被截取的字符串
*/
let str = 'I am fendy,';
let ans = str.substr(7, 5);
console.log(ans);    // fendy

# split

/*
作用:把一个字符串分割成字符串数组
参数:(字符串或正则表达式,返回的数组的最大长度)
返回值:新数组
注意:如果不设置参数就会将每个字符分割
*/
let str="How are you doing today?";
let ans=str.split(" ");
console.log(ans);    // [How,are,you,doing,today?]

# replace

/*8、replace()
作用:用一些字符串取代另一些字符串
参数:(searchvalue,newvalue)    searchvalue:字符串或者正则表达式
返回值:新字符串
*/
let str="I love Fendy";
let ans=str.replace('Fendy','sin');
console.log(ans);    // I love sin

# toUpperCase/toLowerCase

/*
作用:把字符串转成大写/小写
参数:无
返回值:新字符串
*/
let str="How are you doing today?";
let ans=str.toUpperCase();
console.log(ans);    // HOW ARE YOU DOING TODAY?

# 包装类型

Boolean、Number、String这三种原始类型可把原始类型的值包装成对象,比如在字符串调用函数时,引擎会将原始类型的值转换成只读的包装对象,执行完函数后就销毁。

装箱: 即原始类型转换为包装类型。

拆箱: 即包装类型转换为原始类型。

let name = new Number('123');
console.log(typeof name);    //object
console.log(typeof name.toString());    //string
console.log(typeof name.valueOf());        //number

**valueOf : ** 返回最适合该对象类型的原始值。

**toString : ** 将该对象的原始值以字符串形式返回。

在数值运算里,会优先调用valueOf(),如a + b; 在字符串运算里,会优先调用toString(),如alert(c)。

// Array
let arr=[3,5,[1, 2, [3, 4], 5, 6],5]
arr.valueOf()    // [ 3, 5, [ 1, 2, [ 3, 4 ], 5, 6 ], 5 ]
arr.toString() // '3,5,1,2,3,4,5,6,5'

// Object
let person={ name:'Fendy',age: 16 }
person.toString()    // '[object Object]'
person.valueOf()    // { name: 'Fendy', age: 16 }

相关题目

//让 a == 1 && a == 2 && a == 3 为 true
const a = {
    value:[3,2,1],
    valueOf:function(){
        return this.value.pop()
    } // 每次调用,删除一个元素
}

console.log(a == 1 && a == 2 && a == 3) // true (注意仅能判断一次)

# 运算符

# 逻辑位运算

# 位与(&)

每个比特位都为1,则结果为1,否则为0

1 0 1    // 5
1 0 0    // 4
1 0 0    // 4

# 位或(|)

每个比特位都为0,则结果为0,否则为1

1 0 0    // 4
1 0 1    // 5
1 0 1    // 5

# 位异或(^)

每个比特位相同为0,相异为1

1 0 0    // 4
1 0 1    // 5
0 0 1    // 1

# 按非位(~)

对每一个比特位执行非(NOT)操作。NOT a 结果为 a 的反转(即反码)。

const a = 5;     // 00000000000000000000000000000101

console.log(~a); // 11111111111111111111111111111010

# 移位运算

# 左移(<<)

1001 // 9
9 << 2    // 100100
9*2^2

# 有符号右移(>>)

1001    // 9
9 >> 2 // 2

# 无符号右移(>>>)

# 展开运算符

# 某些场景中可以替代apply

let arr = [1, 2, 3, 4, 5];

console.log(Math.max.apply(null, arr));    //ES5
console.log(Math.max(...arr));    //ES6

# 代替数组的pushconcat等方法

let arr = [1, 2, 3, 4, 5];
let arr1 = [6, 7, 8, 9];

//将一个数组追加到另外一个数组中
Array.prototype.push.apply(arr, arr1);    //ES5
arr.push(...arr1)    //ES6
//arr为[1, 2, 3, 4, 5 , 6, 7, 8, 9]

# 浅拷贝数组或对象

let arr = [1, 2, 7, 9, 5];
let copyArr = [...arr];    //copyArr: [1, 2, 7, 9, 5]

# 将伪数组转化为数组

let nodeList = document.querySelectorAll('div');
// querySelectorAll 方法返回的是一个 nodeList 对象。它不是数组,而是一个类似数组的对象。
console.log([...nodeList]) // [div,div,div,...] 

# 剩余运算符

# 作为参数使用

rest参数可理解为剩余的参数,所以必须在最后一位定义

function add(a, ...b) {
    //do sth.
}

# 与解构赋值组合使用

let array = [1,2,3,4,5,6]
let [a,b,..c]=array;    //c:[3,4,5,6]

# 继承

# 继承方式

# 盗用构造函数继承

function Person(name) {
    this.name = name;
}

function Girl() {
      // 继承 Person
    Person.call(this, 'Fendy');
      // 或者
      // Person.apply(this, 'Fendy)
}

let girl = new Girl();
console.log(girl.name);    // Fendy

优点:

  1. 可以传参
  2. 避免属性被所有实例共享

**缺点:**无法获取父元素的方法。

# 原型链继承

function Person() {
    this.name = 'name';
    function say() {

    }
}

Person.prototype.say = function () {
    console.log("Hi,Iam~")
};
function Girl() {

}
// 继承 Person
Girl.prototype = new Person();
let girl = new Girl();
console.log(girl.say());    // Hi,Iam~

**优点:**可以获取父类的方法。

缺点:

  1. 引用类型的属性被所有实例共享
  2. 不可传递参数

# 组合继承

function Person(name) {
    this.name = name;
    this.habits = ['swimming', 'basketball'];
}

Person.prototype.say = function () {
    console.log('Iam'+this.name)
};

function Girl() {
   // 继承属性
    Person.call(this, 'Fendy');
}

// 继承方法
Girl.prototype = new Person();
let girl = new Girl();
let girl2 = new Girl();
girl2.habits.push('eat');
console.log(girl2.habits);    // ["swimming", "basketball", "eat"]
console.log(girl.habits);    // ["swimming", "basketball"]

# 原型式继承

模拟ES5中Object.create的实现,将传入对象作为创建对象的原型。本质上,object()是对传入的对象执行了一次浅复制。

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

// 规范化
let person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person, {
  name: {
    value: "Greg"
  }
});

原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。

# 寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

function createObj (o) {
  let clone = Object.create(o);
  clone.sayName = function () {
    console.log('hi');
  }
  return clone;
}

# 组合寄生继承

  1. 创建父类原型副本
  2. 为创建的副本增加constructor属性,从而弥补了因为重写原型而失去的默认的constructor属性。
  3. 将创建的对象赋值给子类型的原型。
function inheritPrototype(subType,superType){
    var prototype = Object(superType.prototype);//创建对象
    prototype.constructor = subType;//增强对象
    subType.prototype = prototype;//指定对象
}

# 继承缺点

无法决定继承哪些属性,所有属性都得继承。

# 解决方案

用组合的方式,先设计一系列属性或者方法,然后将这些属性方法进行拼装,来形成不同的实例或者类。

function drive(){
  console.log("wuwuwu!");
}
function music(){
  console.log("lalala!")
}
function addOil(){
  console.log("哦哟!")
}

let car = compose(drive, music, addOil);
let newEnergyCar = compose(drive, music);

# 内置对象

# history

back() (opens new window) 加载 history 列表中的前一个 URL
forward() (opens new window) 加载 history 列表中的下一个 URL
go() (opens new window) 加载 history 列表中的某个具体页面

H5新方法:

# pushState

# replaceState

# popstate

# Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

# 属性

  1. constructor //构造函数
  2. size //长度

# 操作方法

  1. add //添加某个值,返回 Set 结构本身
  2. delete //删除某个值,返回一个布尔值
  3. has //返回一个布尔值,表示该值是否为Set的成员
  4. clear //清除所有成员,没有返回值

# 遍历方法

  1. keys //返回键名的遍历器
  2. values //返回键值的遍历器
  3. entries //返回键值对的遍历器
  4. forEach //使用回调函数遍历每个成员
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}

# WeakSet

# 与Set区别

  1. WeakSet 的成员只能是对象,而不能是其他类型的值。
  2. WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用。WeakSet 的成员是不适合引用的,因为它会随时消失。

# 方法

weakset只有3个方法,并且没有size属性。所以,不能遍历它。

  1. add
  2. delete
  3. has

# 作用

WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。

# Map

Map是ES6新增的 数据结构,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。key是跟内存地址绑定的。

# 属性

  1. size
  2. constructor

# 操作方法

  1. set(key,value)
  2. get(key) //如果找不到key,就返回undefined。
  3. has(key)
  4. delete(key)
  5. clear()
const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);
作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。

map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"


Map构造函数接受数组作为参数,实际上执行的是下面的算法。
items.forEach(
  ([key, value]) => map.set(key, value)
);

# 遍历方法

  1. keys
  2. values
  3. entries
  4. forEach
const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"


map.forEach(function(value, key, map) {
    console.log(key + "-" +value );
});
// name-张三
// title-Author

# WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。

  1. WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
  2. WeakMap的键名所指向的对象,不计入垃圾回收机制。只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存,不用手动删除。

# JS事件

# 事件捕获

事件从最不精准的目标(document对象)开始触发,然后到最精确的目标 (不精确 → 精确)

# 事件冒泡

事件按照从最特定的事件目标到最不特定的事件目标(document对象)的顺序触发 (特定 → 不特定)

不支持冒泡的事件:

  1. 鼠标事件:mouseleave、mouseenter
  2. 焦点事件:blur、focus
  3. UI事件:scroll、resize

事件先冒泡后捕获: 对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件时,先暂停执行,直到冒泡事件被捕获后再执行事件。

# 事件委托

事件委托,就是利用事件冒泡的原理,把自己所触发的事件交给父元素代替执行。即:不在事件(直接DOM)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生在哪一个子元素上来做出不同的响应。

好处:

  1. 提高性能
  2. 新添加的元素也能触发绑定在父元素上的监听事件

# 事件循环机制

# 为什么JS是单线程

JS作为主要运行在浏览器的脚本语言,主要用途之一就是操作DOM。

如果JS同时有两个线程,同时对同一个DOM进行操作,这时浏览器不知道应该听谁。为了避免这种问题,所以就把JS设计成单线程语言。

# 执行栈

当执行某个函数、事件(指定过回调函数)时,就会进入执行栈中,等待主线程读取。

# 主线程

js 引擎执行的线程,主要负责页面渲染、函数处理。

主线程循环: 即主线程会不停的从执行栈中获取事件,然后执行完所有栈中的同步代码。

任务队列: 当遇到一个异步事件后,主线程会将这个事件挂在任务队列中。当主线程将执行栈中的所有代码都执行完后,主线程将会去查看任务队列中是否存在任务。 如果存在,那么主线程会依次执行那些任务队列中的回调函数。

异步运行机制:

  1. 所有任务都在主线程上执行,形成一个执行栈。
  2. 主线程之外,还存在一个任务队列。只要异步任务有了返回结果,就在任务队列之中放置一个事件。
  3. 当执行栈中的所有同步任务执行完毕,就会去查看任务队列,那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
  4. 主线程会重复第三点。

# 宏任务和微任务

宏任务:

script(整体代码),setTimeout,setInterval,requestAnimationFrame(有争议),setImmediate,UI渲染,I/O流操作,postMessage,MessageChannel

微任务:

存储在宏任务队列中,当宏任务执行完了回去检查有没有微任务需要执行。如果没有就直接执行下一个宏任务。


Promise.then/reject,MutaionObserver,process.nextTick、fetchAPI、V8垃圾回收过程

微任务的好处:

  1. 采用异步回调替代同步回调解决了浪费 CPU 性能的问题。
  2. 放到当前宏任务最后执行,解决了回调执行的实时性问题。

Eg:

console.log('start');
setTimeout(() => {
  console.log('timeout');
});
Promise.resolve().then(() => {
  console.log('resolve');
});
console.log('end');

/*
start
end
resolve
timeout
*/
  1. 刚开始整个脚本作为一个宏任务来执行,对于同步代码直接压入执行栈。
  2. setTimeout 作为一个宏任务放入宏任务队列。
  3. Promise.then作为一个为微任务放入到微任务队列。
  4. 当本次宏任务执行完,检查微任务队列,发现一个Promise.then,执行。
  5. 接下来进入到下一个宏任务——setTimeout, 执行。

# 事件循环

一次事件循环只执行处于 Macrotask 队首的任务,执行完成后,立即执行 Microtask 队列中的所有任务。

Event Loop(事件循环)中,每一次循环称为 tick, 每一次tick的任务如下:

  1. 一开始整体脚本作为一个宏任务执行。
  2. 执行过程中,如果遇到宏任务进入宏任务队列,微任务进入微任务队列。
  3. 当前的宏任务出队,检查是否存在 Microtask,如果存在则不停的执行,直至清空 microtask 队列。
  4. 执行浏览器UI线程的渲染工作。(每一次事件循环,浏览器都可能会去更新渲染)。
  5. 检查是否有Web Worker
  6. 重复以上步骤。

# 自定义事件

  1. 创建自定义事件

  2. 初始化自定义事件

  3. 监听自定义事件

  4. 触发自定义事件

  // 方式一
  // let myEvent = new Event('myEvent');
  // 方式二
  // let myEvent = new CustomEvent('myEvent', {
  //   detail: {
  //     name: 'lindaidai'
  //   }
  // })
  // 方式三
  let myEvent = document.createEvent('CustomEvent');
  myEvent.initEvent('myEvent', true, true)

  let btn = document.getElementsByTagName('button')[0]
  btn.addEventListener('myEvent', function (e) {
    console.log(e)
    console.log(e.detail)
  })
  setTimeout(() => {
    // 触发事件
    btn.dispatchEvent(myEvent)
  }, 2000)

# 内存泄露

内存泄漏 在使用一些内存之后,如果后面不再需要用到这些内存,但没有将它们及时释放掉,就称为内存泄漏。

# 常见的内存泄露

# 意外的全局变量

未定义的变量会在全局对象创建一个新变量。

function f() {
    eat = "fish";    //未定义会创建在全局中
}
f();

解决方案: 在JavaScript头部加上use strict,使用严格模式避免意外的全局变量

# 被遗忘的定时器或回调函数

定义定时器(setTimeout / setInterval)后没有移除(clearTimeout / clearInterval)

# 闭包

# DOM元素的引用

闭包的关键是匿名函数可以访问父级作用域的变量,让变量不被回收。

# 垃圾回收机制

# 引用计数

跟踪每个值被引用的次数,声明一个变量后,这个变量每被其他变量引用一次,就加 1 ,如果变量引用释放了,就减 1,当引用次数为 0 的时候,对象就被清理。但这个有个循环引用的弊端,所以应用的比较少。

# 标记清除

垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量以及被环境中的变量引用的标记,而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。

# 垃圾收集性能优化

# 并发与并行

# 并发

宏观概念,指一段时间内通过任务切换完成两个任务,这种情况称为并发。

# 并行

微观概念,指同时执行多个任务的情况。

# 阻塞

# 同步阻塞

小明一直盯着下载进度条,到 100% 的时候就完成。

js就是属于这种类型的

# 同步非阻塞

小明提交下载任务后就去干别的,每过一段时间就去瞄一眼进度条,看到 100% 就完成。(轮询)

# 异步阻塞

小明换了个有下载完成通知功能的软件,下载完成就“叮”一声。不过小明仍然一直等待“叮”的声音

# 异步非阻塞

仍然是那个会“叮”一声的下载软件,小明提交下载任务后就去干别的,听到“叮”的一声就知道完成了。

# 模块化

# ES6模块

  1. 编译时加载。Es 6模块不是对象,而是通过export命令显式指定输出的代码。在import时可以指定加载某个输出值,而不是加载整个模块。
  2. 模块输出的是值的引用。它是动态引用的,并且不会缓存值,模块里面的变量绑定其所在的模块。

# CommonJS

  1. 模块是运行时加载。CommonJs模块就是对象,即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法。
  2. 模块输出的是一个值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值。

# AMD

# 柯里化函数

# 柯里化

在一个函数中,首先会接受一些参数,然后再返回一个新的函数的技术,称为函数的柯里化。

作用:通常可用于在不侵入函数的前提下,为函数 预置通用参数,供多次重复调用。

const add = function add(x) {
    return function (y) {
        return x + y
    }
};
let add1 = add(2);
console.log(add1(3));    // 5

# AOP

AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。

通常,在 JavaScript 中实现 AOP,都是指把一个函数“动态织入”到另外一个函数之中,具体的实现技术有很多,本节我们通过扩展 Function.prototype 来做到这一点。

# requestAnimationFrame

告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用window.requestAnimationFrame()

# 实现currying函数

# 异步编程

# 回调函数

优点: 简单、容易理解。

**缺点:**不利于代码阅读和维护,各部分之间高度耦合,而且每个任务只能指定一个回调函数。

# Promise

# 概念

Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。本质上Promise是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。

  1. 三态:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected),状态的改变只能是单向的,且变化后不可在改变。
  2. 一个 Promise 必须提供一个 then 方法以访问其当前值、终值和据因,且返回 promise 对象。
function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done~');
  });
}
timeout(100).then((value) => {
  console.log(value);
});


let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});
promise.then(function() {
  console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved

# 解决回调地狱

由于它的then方法和catch、finally方法会返回一个新的Promise,所以可以允许我们链式调用,解决传统的回调地狱问题。

# 原理

Promise的原理是状态管理。Promise一共由三种状态: pending、fulfilled、rejected, 而状态是不可逆的,Promise正是通过修改状态的方式,在合适的时机触发相应状态的回调来达到处理异步的目的。通过then的链式调用也避免了回调地域的问题。

# finally

# all && race

# Promise.all()

Promise.all()的作用是接收一组异步任务,然后并行执行异步任务,并且在所有异步操作执行完后才执行回调。

  1. 如果一组异步操作中有一个异常都不会进入.then()的第一个回调函数参数中。

# Promise.race()

Promise.race()的作用也是接收一组异步任务,然后并行执行异步任务,只保留取第一个执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃

# Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态。Generator 函数是一个状态机,封装了多个内部状态。

function* helloWorldGenerator() {
    yield 5+15;
    yield 'world';
    return 'ending';
}

let hw = helloWorldGenerator();
console.log(hw.next());    // {value: 20,done: false}
console.log(hw.next());    // {value: world,done: false}
console.log(hw.next());    // {value: ending,done: true}

# async/await

async是ES2017引进的函数,是Generator函数的语法糖。它将Generator函数的*号替换成async,将yield替换成await。

# async

async函数内部return语句返回的值,会成为then方法回调函数的参数。只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

async function f() {
    return 'Fendy';
}

f().then(value => {
    console.log(value);    // Fendy
});

# await

  1. await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
//简易的sleep版本
function sleep(interval) {
    return new Promise(resolve => {
        setTimeout(resolve, interval);
    })
}

//  用法
async function one2FiveInAsync() {
    for(let i = 1; i <= 5; i++) {
        console.log(i);
        await sleep(1000);
    }
}

one2FiveInAsync();
  1. await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。
async function f() {
  await Promise.reject('出错了');
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了
  1. 任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。
async function common () {
  await Promise.reject('error~')
  return Promise.resolve('Fendy')
}
  1. 紧跟着await后面的语句相当于放到了new Promise中,下一行及之后的语句相当于放在Promise.then中。

# 错误处理

有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。

async function f() {
  try {
    await Promise.reject('出错了');
      } catch(e) {}
  return 'hello world';
}

f().then(v => console.log(v))
// hello world

# Ajax

Ajax是可以实现在不必刷新整个页面的情况下实现局部更新,与服务器进行异步通讯的技术。

# XMLHTTPRequest

# 属性

# readyState

0:未初始化(Uninitialized)。尚未调用 open()方法。 1:已打开(Open)。已调用 open()方法,尚未调用 send()方法。 2:已发送(Sent)。已调用 send()方法,尚未收到响应。 3:接收中(Receiving)。已经收到部分响应。 4:完成(Complete)。已经收到所有响应,可以使用了。

# status

服务器返回的HTTP状态码

# API

# open()

创建http请求

  • 第一个参数:定义请求的方式(get/post)
  • 第二个参数:提交的地址
  • 第三个参数:是否异步(默认为true)
# send()
# setRequestHeader()
# abort()
# getAllResponseHeaders()
# getResponseHeader()

# Ajax原理

// 1、创建XMLHttpRequest对象
let xhr = new XMLHttpRequest()
// 2、监听状态改变
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
      alert(xhr.responseText)
    } else {
      alert('Request was unsuccessful: ' + xhr.status)
    }
  }
}
// 3、建立xhr請求
xhr.open('get', 'example.txt', true)
// 4、发送请求
xhr.send()

另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。

async function f() {
  await Promise.reject('出错了')
    .catch(e => console.log(e));
  return await Promise.resolve('hello world');
}

f().then(v => console.log(v))
// 出错了
// hello world

# 防抖节流

# 防抖

触发高频事件后n秒内函数只会执行一次,如果n秒内高频事件再次被触发,则重新计算时间。

eg:当持续触发scroll事件时,事件处理函数handle只在停止滚动1000毫秒之后才会调用一次,即持续触发滚动事件的过程中,handle一直没有执行。

const debounce = (handleFn, time, args) => {
  let timeout = null
  return function () {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      handleFn.apply(this, args)
    }, time)
  }
}

应用场景

  1. input实时搜索
  2. 浏览器的resize
  3. scroll

# 节流

原理: 将即将被执行的函数用setTimeout 延迟一段时间执行。如果该次延迟执行还没有完成,则忽略接下来调用该函数的请求。

const throttle = (handleFn, time, args) => {
  let canRun = true
  return function () {
    if (!canRun) { return }
    // 防止连续执行
    canRun = false
    setTimeout(() => {
      handleFn.apply(this, args)
      canRun = true
    }, time)
  }
}

应用场景

  1. 表单重复提交
  2. 滚动加载

# 正则表达

# 匹配邮箱

const email = 'zf@5.cn'
const cmp=/[a-zA-Z0-9-_]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+/
console.log(cmp.test(email))

# 匹配手机号码

const regex= /^1[3-9]\d{9}$/
console.log(regex.test('15625701754'))

# 根据name获取cookie中的值

// (^| )以''或' '开头
// ([^;]*)匹配不包括;的所有内容
const getCookieByName = (name) => {
  const cookie =cookies.match(new RegExp(`(^| )${name}=([^;]*)`))
  if (cookie) { return decodeURIComponent(cookie[2]) }
}

# 获取Url参数

// [^?&]查找任何不在方括号之间的字符
const getUrlQuery = (url) => {
  const search = url.split('?')[1]
  const reg =/([^?&=]+)=([^&]+)/g
  if (!search) {
    return {}
  }
  const q = {}
  url.replace(reg, (_, k, v) => {
    q[k] = v
  })
  return q
}

# 手写题

# 浅拷贝&深拷贝

浅拷贝: 复制的是变量的地址,如果原地址指向的内容发生改变,那么浅拷贝出来的对象也会发生相应的改变。

深拷贝: 增加了一个指针并且申请了一个新的内存,使这个增加的指针指向这个新的内存。

# 浅拷贝(如下方法第一层是深拷贝)

# Object.assign

Object.assgin只会拷贝所有的属性值到新的对象中,但如果属性值是一个对象的话,拷贝的是地址,所以并不是深拷贝。

let obj = {
    a: 1,
    b: {
        food: 'food',
        bar: 'bar'
    }
};

let copyObj = Object.assign({}, obj);
console.log(copyObj);    
//{a:1,b:{food:'food',bar:'bar'}}

obj.a = 4;
copyObj.b.food = 'fish';

console.log(copyObj.a);    
// 1 -> 拷贝的是值,不会随着obj修改而修改。
console.log(obj.b.food);    
// fish -> 拷贝的是地址,obj和copyObj的地址指向同一个值,所以只要其中一个修改了,另外的也会跟着修改
# 展开运算符

展开运算符(...)与Object.assign一样:拷贝的属性值为对象时,只拷贝地址。

let obj = {
    a: 1,
    b: {
        food: 'food',
        bar: 'bar'
    }
};

let copyObj = {...obj};
console.log(copyObj);    //{a:1,b:{food:'food',bar:'bar'}}

obj.a = 4;
copyObj.b.food = 'fish';

console.log(copyObj.a);        //1
console.log(obj.b.food);    //fish
# slice
const arr=[1,[2,3,4],5]
const newArr=arr.slice()
newArr[0]=0
newArr[1][1]=6

console.log(arr); // [ 1, [ 2, 6, 4 ], 5 ]
console.log(newArr);// [ 0, [ 2, 6, 4 ], 5 ]
# concat

同上

let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);//[ 1, 2, 3 ]
# push+apply
let arr=[1,2,3],newArr=[];
[].push.apply(newArr,arr);
# 手动实现
const shallowClone = (target) => {
  if (target !== null && typeof target === 'object') {
    const cloneTarget = Array.isArray(target) ? [] : {}
    for (const targetKey in target) {
      cloneTarget[targetKey] = target[targetKey]
    }
    return cloneTarget
  } else {
    return target
  }
}

const Fendy = {
  name: 'Fendy',
  age: 18,
  likes: [
    {
      name: 'food',
      age: 16
    },
    {
      name: 'water',
      age: 18
    }
  ]
}

const nowFendy = shallowClone(Fendy)
nowFendy.likes[0].age = 21
nowFendy.name = 'nowFendy'

console.log(Fendy)
/**    
{
  name: 'Fendy',
  age: 18,
  likes: [ { name: 'food', age: 21 }, { name: 'water', age: 18 } ]
}
**/

console.log(nowFendy)
/**
{
  name: 'nowFendy',
  age: 18,
  likes: [ { name: 'food', age: 21 }, { name: 'water', age: 18 } ]
}
**/

# 深拷贝

# JSON.stringify
let person = {
    age: 18,
    lover:{
        name: 'sn'
        }
    };
let deepCopy = JSON.parse(JSON.stringify(person));
console.log(deepCopy);    // age: 18,lover:{ name: 'sn' }

deepCopy.lover.name = 'ky';

console.log(deepCopy.lover.name);    //    ky
console.log(person.lover.name);        //    sn
# 递归
  1. 判断参数是否为对象
  2. 创建深拷贝副本
  3. 循环参数的key,并且判断value是否为Object
  4. 返回深拷贝副本
const deepCopy = (obj) => {
  if (obj !== null && typeof obj === 'object') {
    const deepObj = {};
    for (const objKey in obj) {
      if (typeof obj[objKey] === 'object') {
        deepObj[objKey] = deepCopy(obj[objKey])
      } else {
        deepObj[objKey] = obj[objKey]
      }
    }
    return deepObj
  }else {
    return obj
  }
}

# Object.create

创建一个新对象,使用现有的对象来提供新创建的对象的__proto__(会返回一个新对象,带着指定的原型对象和属性)

  1. 创建新函数
  2. 将新函数的原型绑定指定的值
  3. 返回新函数的实例
// 手写 Object.create()
function _create(obj) {
  if (obj === null && typeof obj != 'object') {
    throw new Error(`${obj}不是有效的对象`);
  } else {
    function Temp() {}
    Temp.prototype = obj;
    return new Temp;
  }
}

# instanceof

  1. 判断参数是否为对象
  2. 获取参数的原型对象
  3. 通过循环不断更新原型对象,同时判断原型对象是否等于第二个参数的原型
function myInstanceof(left,right) {
    // 如果是基本数据类型,直接返回false
    if (left==null || typeof left!=='object') return false;
    //getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
    let proto = Object.getPrototypeOf(left);
    while (true) {
        if (proto===null) return false;
        if (proto===right.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
}

# new

  1. 创建新对象
  2. 将新对象的原型设置构造函数的prototype对象
  3. 将构造函数的作用域赋给新的对象,并执行构造函数
  4. 返回新对象
function _new(Fn, ...args) {
    const obj = {};//1、创建新的对象
         obj.__proto__ = Fn.prototype;//2、将新对象的原型关联到构造函数的原型对象
    const res = Fn.call(obj, ...args);//3、让构造函数的this指向新的对象,并执行构造函数
    return typeof res === 'object' ? res : obj;//4、如果构造函数没有返回值,就返回创建的对象。
}

function Person(age) {
    this.age = age;
}
Person.prototype.say = function (age) {
    console.log("Your age is " + this.age)
}
let person = _new(Person,13)
person.say()

# apply、call、bind

# apply

Function.prototype._apply = function (context, args) {
    if (typeof this !== 'function') {
        // 调用的若不是函数则报错
        throw new TypeError('Error');
    }
      let result = null;
    // context为null时,context设置为window
    context = context || window;
    // 将调用函数设为对象的方法
    context.fn = this;
    // 调用函数
  if(arguments[1]){
    result = context.fn(...arguments[1]);
  }else {
    result = context.fn();
  }
    // 删除该方法
    delete context.fn;
    return result;
};

# call

Function.prototype._call = function (context) {
  if (typeof this !== 'function') {
    return new Error('Error')
  }
  let res = null, args = [...arguments].slice(1)
  context = context || window
  context.fn = this
  res = context.fn(...args)
  delete context.fn
  return res
}

# Ajax

// 1、创建XMLHttpRequest对象
let xhr = new XMLHttpRequest()
// 2、监听状态改变
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
      alert(xhr.responseText)
    } else {
      alert('Request was unsuccessful: ' + xhr.status)
    }
  }
}
// 3、建立xhr請求
xhr.open('get', 'example.txt', true)
// 4、发送请求
xhr.send()

# 各种区别

# null & undefined

  • 语义不一样。null表示定义了,但是值为空。undefined表示未定义。

  • 转换成数字结果不一样。Number(null)为0,Number(undefined)为NaN。

# Object.is()和===

Object在严格等于的基础上修复了一些特殊情况下的失误,具体来说就是+0和-0,NaN和NaN。

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

# class和function

  1. **语义上:**class 也是一个语法糖,本质还是基于原型链,class 语义化和编码上更加符合面向对象的思维。
  2. **改变执行上下文:**function可使用call、apply、bind来改变执行上下文,但是class不可以。因为class在转换成ES5的时候做了一层代理,来禁止这种行为。

# isNaN和Number.isNaN

  • isNaN:除了判断NaN为true,还会把不能转成数字的判断为true,例如'xxx'
  • Number.isNaN:只有判断NaN时为true,其余情况都为false

# Object.assign和扩展运算符

相同点:两者都是浅拷贝

不同点

  • Object.assign()方法接收的第一个参数作为目标对象,后面的所有参数作为源对象。然后把所有的源对象合并到目标对象中。它会修改了目标对象,因此会触发 ES6 setter。
  • 扩展运算符返回参数的浅拷贝对象,不会改变参数本身。不复制继承的属性或类的属性,但是它会复制ES6的 symbols 属性。

# encodeURI和encodeURIComponent

  • encodeURI对整个URL进行编码,而URL的特定标识符不会被转码。

  • encodeURIComponent,对URL中的参数进行编码,因为参数URL也可以是一个参数,所以会对URL的特定标识符进行转码。

    const url = 'https://blog.fendy5.cn/login?redirect=https://blog.fendy5.cn/s/算法'
    
    console.log(encodeURI(url))
    // https://blog.fendy5.cn/login?redirect=https://blog.fendy5.cn/s/%E7%AE%97%E6%B3%95
    console.log(encodeURIComponent(url))
    // https%3A%2F%2Fblog.fendy5.cn%2Flogin%3Fredirect%3Dhttps%3A%2F%2Fblog.fendy5.cn%2Fs%2F%E7%AE%97%E6%B3%95
    

# typeof和instanceof

# 一些bug

# 0.1+0.2!=0.3

原因:

由于计算机是通过二进制来存储数据,很多十进制小数用二进制表示都会是无限循环的。因为JS采用浮点数标准,导致会裁剪掉我们的数字。裁剪后会出现精度消失的问题,所以0.1就不再是0.1了。

解决:

# [] == ![] 为何为true

由于 ! 的优先级高于 ==![] 首先会被转换为false,然后根据Boolean转换原则,false将会转换为Number类型,即转换为 0,然后左侧的 [] 转换为原始类型,也为 0 ,所以最终结果为 true

# [] ===[] 为何为false

运算符对于对象(数组也是对象)只看双方地址,地址一样则返回true。

#

Last Updated: 4/21/2022, 5:35:47 PM