JS

394 阅读1小时+

JS基础

一级

1. new操作符的实现原理

new操作符的执行过程:

(1)创建了一个新的空对象

(2)将对象的原型设置为函数的 prototype 对象。

(3)函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)

(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

具体实现:

function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判断参数是否是一个函数
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一个空对象,对象的原型为构造函数的 prototype 对象
  newObject = Object.create(constructor.prototype);
  // 将 this 指向新建对象,并执行函数
  result = constructor.apply(newObject, arguments);
  // 判断返回对象
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判断返回结果
  return flag ? result : newObject;
}
// 使用方法
objectFactory(构造函数, 初始化参数);
        function Person(name, age) {
            this.name = name
            this.age = age
        }
        function myNew(fn, ...args) {
            // 创建一个空对象,将构造函数的原型设置为对象的原型
            let obj = Object.create(fn.prototype)
            // 构造函数的this指向创建的对象
            const result = fn.apply(obj, args)
            // 判断函数返回类型,值类型返回创建的对象,引用类型返回引用类型对象
            if (result && typeof result === 'object' || typeof result === 'function') {
                return result
            }
            return obj
        }
        console.log(myNew(Person, '测试new', 20))

2. 什么是 DOM 和 BOM?

  • DOM 文档对象模型,把文档当做一个对象,主要定义了处理网页内容的方法和接口。 以树状结构对 HTML 文档结构的表示。并提供了一组方法和属性,用于操作和访问这些元素。
  • BOM 浏览器对象模型,把浏览器当做一个对象,主要定义了与浏览器进行交互的法和接口。BOM的核心是 window,它既是通过 js 访问浏览器窗口的一个接口,又是一个全局对象。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。

3. 对类数组对象的理解,如何转化为数组

类数组对象是一种特殊的对象,具有类似数组的数字索引和length属性,可以通过索引访问和操作元素,不能调用数组的方法。

  • 类数组产生的原因

从DOM获取的集合:

<p>标签</p>
<p class="example">标签</p>
通过querySelectorAll获取的NodeList对象 

let paragraphs = document.querySelectorAll('p');

console.log(paragraphs); 输出一个NodeList对象   NodeList [p]

从getElementsByClassName获取的HTMLCollection对象 

let elements = document.getElementsByClassName('example'); 

console.log(elements); 输出一个HTMLCollection对象 HTMLCollection [p.example]

函数参数arguments对象:

   function myFunction() {
      // console.log(arguments)

      // 输出一个类数组对象 Arguments(3) [1, 'hello', true, callee: ƒ,Symbol(Symbol.iterator):ƒ]
        }
   myFunction(1, 'hello', true); 
  • 转换为数组方法

Array.from('伪数组名')

  let arr = []
     function myFunction() {
          arr = arguments
       }
     myFunction(1, 'hello', true);
    Array.from('伪数组名')
    let arr1=Array.from(arr)
    console.log(arr1); [1, 'hello', true]

Array.prototype.slice.call('伪数组名')

 Array.prototype.slice.call('伪数组名')
 let arr1=Array.prototype.slice.call(arr)

ES6的展开操作符

let arr1=[...arr]
console.log(arr1);

Array.prototype.concat.apply([],'伪数组名')

Array.prototype.concat.apply([],'伪数组名')
let arr1=Array.prototype.concat.apply([],arr)

Array.prototype.map.call('伪数组名',(参数)=>{return 参数})

Array.prototype.map.call('伪数组名',(参数)=>{return 参数})
let arr1=Array.prototype.map.call(arr,(item)=>{
     console.log(item);伪数组中的每一项
        return item
 })

4. 对AJAX的理解,实现一个AJAX请求

Ajax 使网页实现异步更新。可以在不重新加载整个网页的情况下,实现局部更新。

创建AJAX请求的步骤:

  • 创建一个 XMLHttpRequest 对象。
  • 使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
  • 添加一些信息和监听函数。通过 setRequestHeader 方法来设置请求头信息。状态变化时会触发onreadystatechange 事件,当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。
  • 调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
  console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);

使用Promise封装AJAX:

// promise 封装实现:
function getJSON(url) {
  // 创建一个 promise 对象
  let promise = new Promise(function(resolve, reject) {
    let xhr = new XMLHttpRequest();
    // 新建一个 http 请求
    xhr.open("GET", url, true);
    // 设置状态的监听函数
    xhr.onreadystatechange = function() {
      if (this.readyState !== 4) return;
      // 当请求成功或失败时,改变 promise 的状态
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    // 设置错误监听函数
    xhr.onerror = function() {
      reject(new Error(this.statusText));
    };
    // 设置响应的数据类型
    xhr.responseType = "json";
    // 设置请求头信息
    xhr.setRequestHeader("Accept", "application/json");
    // 发送 http 请求
    xhr.send(null);
  });
  return promise;
}

5. JavaScript为什么要进行变量提升,它导致了什么问题?

变量提升指:在函数中声明的变量,被提升到了函数的首部,可以在变量声明前访问到而不会报错。

本质原因是代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链指向的是当前执行上下文的变量对象

JS在拿到一个变量或者一个函数的时候,会有两步操作,即解析和执行。

  • 在解析阶段,解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。在函数执行之前,也会创建一个函数执行上下文环境,this、arguments和函数的参数。

    • 全局上下文:变量定义,函数声明
    • 函数上下文:变量定义,函数声明,this,arguments
  • 在执行阶段,按照代码的顺序依次执行。

(1)提高性能 在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。如果没有这一步,那么每次执行代码前都必须重新解析

(2)容错性更好

变量提升可以在一定程度上提高JS的容错性,看下面的代码:

a = 1;var a;console.log(a);

如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。

虽然,在可以开发过程中,可以完全避免这样写,但是有时代码很复杂的时候。可能因为疏忽而先使用后定义了,这样也不会影响正常使用。由于变量提升的存在,而会正常运行。

总结:

  • 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
  • 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行

变量提升虽然有一些优点,但是他也会造成一定的问题,在ES6中提出了let、const来定义变量,它们就没有变量提升的机制。下面看一下变量提升可能会导致的问题:

var tmp = new Date();

function fn(){
	console.log(tmp);
	if(false){
		var tmp = 'hello world';
	}
}

fn();  // undefined

在这个函数中,原本是要打印出外层的tmp变量,但是因为变量提升的问题,内层定义的tmp被提到函数内部的最顶部,相当于覆盖了外层的tmp,所以打印结果为undefined。

var tmp = 'hello world';

for (var i = 0; i < tmp.length; i++) {
	console.log(tmp[i]);
}

console.log(i); // 11

由于遍历时定义的i会变量提升成为一个全局变量,在函数结束之后不会被销毁,所以打印出来11。

二级

map和Object的区别

mapobject
键任意类型键字符串类型或者 Symbol 类型
键值对有序键值对无序
方法:size 属性用于获取元素个数forEach() 方法用于遍历元素for in 循环
适合于需要存储大量动态数据的情况适合于保存固定且已知的属性

Map使用

在 JavaScript 中,Map 是一种用来存储键值对的数据结构,你可以使用以下方法来使用 Map

  1. 创建一个新的 Map 对象:

    const myMap = new Map();
    
  2. 添加键值对到 Map 中:

    myMap.set('key1', 'value1');
    myMap.set('key2', 'value2');
    
  3. Map 中获取值:

    console.log(myMap.get('key1')); // 输出: value1
    
  4. 检查 Map 中是否包含某个键:

    console.log(myMap.has('key1')); // 输出: true
    
  5. 删除 Map 中的键值对:

    myMap.delete('key2');
    
  6. 获取 Map 的大小(包含的键值对数量):

    console.log(myMap.size); // 输出: 1
    
  7. 遍历 Map 中的键值对:

    myMap.forEach((value, key) => {
      console.log(key + ' = ' + value);
    });
    

Object使用

在 JavaScript 中,Object 是一种非常常用的数据结构,用于存储键值对。以下是一些使用 Object 的常见方法:

  1. 创建一个新的空对象:

    const myObject = {};
    
  2. 添加属性到对象中:

    myObject.key1 = 'value1';
    myObject['key2'] = 'value2';
    
  3. 从对象中获取属性的值:

    console.log(myObject.key1); // 输出: value1
    console.log(myObject['key2']); // 输出: value2
    
  4. 检查对象中是否包含某个属性:

    console.log('key1' in myObject); // 输出: true
    
  5. 删除对象中的属性:

    delete myObject.key2;
    
  6. 获取对象的所有属性名:

    const keys = Object.keys(myObject);
    console.log(keys); // 输出: ['key1']
    
  7. 获取对象的所有属性值:

    const values = Object.values(myObject);
    console.log(values); // 输出: ['value1']
    

set和map的区别

  • Set 存储唯一的值,没有重复元素。
  • Map 存储键值对,键是唯一的,值可以重复。
  • Set 和 Map 按照插入顺序存储的
  • Set 使用三等运算符(===)进行比较,而 Map 在对象键上使用三等运算符进行比较。
  • Set 和 Map 都提供了类似的方法,如增加、删除和检查元素的方法。

weakSet和weakMap的区别

  • WeakSet 和 WeakMap 是弱引用的数据结构,它们的对象是弱引用的,在没有其他引用时会被垃圾回收。
  • WeakSet 只能存储对象类型的值,而 WeakMap 的键必须是对象类型,值可以是任意类型。
  • WeakSet 和 WeakMap 没有迭代器,无法直接访问其元素。
  • WeakSet 提供了 add()delete() 和 has() 等方法来操作元素。
  • WeakMap 提供了 set()get()delete() 和 has() 等方法来操作键值对。
let user1 = { name: 'Alice' };
let userSet = new WeakSet();

userSet.add(user1);
console.log(userSet.has(user1)); // true

user1 = null; // 销毁对象

console.log(userSet.has(user1)); // false,user1 对象已被自动从集合中删除
let user1 = { id: 1, name: 'Alice' };

let statusMap = new WeakMap();

statusMap.set(user1, 'active');

console.log(statusMap.get(user1)); // "active"

user1 = null; // 销毁对象

console.log(statusMap.get(user1)); // undefined,user1 对应的状态已被自动从集合中删除

2. JavaScript脚本延迟加载的方式有哪些?

延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度。

  • defer 属性: 多个 defer 属性的js文件按顺序执行 ,js文件的加载与文档的加载同步解析,文档加载完成后再执行这个脚本文件

  • async 属性: 多个 async 属性的js文件不按顺序执行,js文件异步加载,当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。

  • 动态创建 DOM 方式: 对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。

  • 使用 setTimeout 延迟方法: 设置一个定时器来延迟加载js脚本文件

  • 让 JS 最后加载: 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。

3.ES6模块与CommonJS模块有什么异同?

ES6模块和CommonJS模块是两种不同的模块系统,它们在很多方面有异同:

异同点:

  1. 语法差异:

    • ES6模块使用importexport关键字来导入和导出模块,
    • CommonJS模块使用require()module.exports来导入和导出模块
  2. 导入导出方式:

    • ES6模块支持静态导入,模块的导入在代码静态分析阶段就能确定。
    • CommonJS模块的导入是动态的,导入语句的执行发生在运行时。
  3. 模块加载时机:

    • ES6模块是在编译时加载,因此模块的依赖关系在代码静态分析阶段就能确定。
    • CommonJS模块是在运行时加载,模块的依赖关系只能在运行时确定。
  4. 默认导出和命名导出:

    • ES6模块可以同时支持默认导出和命名导出。
    • CommonJS模块不直接支持默认导出,但可以通过module.exports来实现类似默认导出的功能。
  5. 浏览器兼容性:

    • ES6模块是原生支持的,可以直接在支持ES6模块的现代浏览器中使用。
    • CommonJS模块是Node.js环境中使用的模块系统,需要借助工具(如webpack或者Browserify)才能在浏览器中使用。

4. 如何判断一个对象是否属于某个类?

  • 第一种方式,使用 instanceof 运算符来判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

  • 第二种方式,通过对象的 constructor 属性来判断,对象的 constructor 属性指向该对象的构造函数,但是这种方式不是很安全,因为 constructor 属性可以被改写。

  • 第三种方式,如果需要判断的是某个内置的引用类型的话,可以使用 Object.prototype.toString() 方法来打印对象的[[Class]] 属性来进行判断。

5.for...in 和for... of区别

  • for…in 获取的是对象的键名,for…of 遍历获取的是对象的键值;

  • for… in 会遍历对象的整个原型链,而 for … of 只遍历当前对象;

  • for…in 会返回数组中所有可枚举的属性,for…of 只返回数组的下标对应的属性值;

  • 总结: for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。

三级

1.map和weakMap的区别

MapweakMap
键是强引用类型,只要键存在于 Map 中,它所引用的对象就不会被垃圾回收键是弱引用类型,键引用的对象如果没有其他引用存在,会被垃圾回收
键可以是任意类型,包括原始类型和对象引用对象
Map 提供了迭代方法 (例如 keys()values()entries()),可以直接迭代 Map 的键值对WeakMap 由于键是弱引用,没有提供直接访问键的迭代方法。
Map 提供了 size 属性,可以获取其存储的键值对数量

由于这些区别,Map 更适用于存储需要长期保存的键值对,而 WeakMap 更适用于存储临时的、不需要长期保存并且键的生命周期由其他的对象决定的键值对。因为 WeakMap 的键是弱引用,所以无法遍历键、无法获取大小,也无法清空整个 WeakMap

2. 常用的正则表达式有哪些?

// (1)匹配 16 进制颜色值
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;

// (2)匹配日期,如 yyyy-mm-dd 格式
var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;

// (3)匹配 qq 号
var regex = /^[1-9][0-9]{4,10}$/g;

// (4)手机号码正则
var regex = /^1[34578]\d{9}$/g;

// (5)用户名正则
var regex = /^[a-zA-Z$][a-zA-Z0-9_$]{4,16}$/;

3. 对JSON的理解

JSON 是一种数据交换格式。

JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等

在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,

  • JSON.stringify 函数,将数据,转换为一个 JSON 字符串。在前端向后端发送数据时,可以调用这个函数将数据对象转化为 JSON 格式的字符串。
  • JSON.parse() 函数,将 JSON 格式的字符串转换为一个 js 数据结构。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。

4. Unicode、UTF-8、UTF-16、UTF-32的区别?

  • Unicode 是字符,而UTF-8UTF-16UTF-32是字符集编码规则;
  • UTF-16 使用变长码元序列的编码方式,相较于定长码元序列的UTF-32算法更复杂,甚至比同样是变长码元序列的UTF-8也更为复杂,因为其引入了独特的代理对这样的代理机制;
  • UTF-8需要判断每个字节中的开头标志信息,所以如果某个字节在传送过程中出错了,就会导致后面的字节也会解析出错;而UTF-16不会判断开头标志,即使错也只会错一个字符,所以容错能力教强;
  • 如果字符内容全部英文或英文与其他文字混合,但英文占绝大部分,那么用UTF-8就比UTF-16节省了很多空间;而如果字符内容全部是中文这样类似的字符或者混合字符中中文占绝大多数,那么UTF-16就占优势了,可以节省很多空间;

5. 常见的位运算符有哪些?其计算规则是什么?

常见的位运算有以下几种:

运算符描述运算规则
&两个位都为1时,结果才为1
``两个位都为0时,结果才为0
^异或两个位相同为0,相异为1
~取反0变1,1变0
<<左移各二进制位全部左移若干位,高位丢弃,低位补0
>>右移各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃

1. 按位与运算符(&)

定义: 参加运算的两个数据按二进制位进行“与”运算。 运算规则:


0 & 0 = 0  
0 & 1 = 0  
1 & 0 = 0  
1 & 1 = 1

总结:两位同时为1,结果才为1,否则结果为0。 例如:3&5 即:

0000 0011 
   0000 0101 
 = 0000 0001

因此 3&5 的值为1。 注意:负数按补码形式参加按位与运算。

用途:

(1)判断奇偶

只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if ((i & 1) == 0)代替if (i % 2 == 0)来判断a是不是偶数。

(2)清零

如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。

2. 按位或运算符(|)

定义: 参加运算的两个对象按二进制位进行“或”运算。

运算规则:

0 | 0 = 0
0 | 1 = 1  
1 | 0 = 1  
1 | 1 = 1

总结:参加运算的两个对象只要有一个为1,其值为1。 例如:3|5即:

0000 0011
  0000 0101 
= 0000 0111

因此,3|5的值为7。 注意:负数按补码形式参加按位或运算。

3. 异或运算符(^)

定义: 参加运算的两个数据按二进制位进行“异或”运算。

运算规则:

0 ^ 0 = 0  
0 ^ 1 = 1  
1 ^ 0 = 1  
1 ^ 1 = 0

总结:参加运算的两个对象,如果两个相应位相同为0,相异为1。 例如:3|5即:

0000 0011
  0000 0101 
= 0000 0110

因此,3^5的值为6。 异或运算的性质:

  • 交换律:(a^b)^c == a^(b^c)
  • 结合律:(a + b)^c == a^b + b^c
  • 对于任何数x,都有 x^x=0,x^0=x
  • 自反性: a^b^b=a^0=a;

4. 取反运算符 (~)

定义: 参加运算的一个数据按二进制进行“取反”运算。

运算规则:

~ 1 = 0~ 0 = 1

总结:对一个二进制数按位取反,即将0变1,1变0。 例如:~6 即:

0000 0110= 1111 1001

在计算机中,正数用原码表示,负数使用补码存储,首先看最高位,最高位1表示负数,0表示正数。此计算机二进制码为负数,最高位为符号位。 当发现按位取反为负数时,就直接取其补码,变为十进制:

javascript
复制代码
0000 0110   = 1111 1001反码:1000 0110补码:1000 0111

因此,~6的值为-7。

5. 左移运算符(<<)

定义: 将一个运算对象的各二进制位全部左移若干位,左边的二进制位丢弃,右边补0。 设 a=1010 1110,a = a<< 2 将a的二进制位左移2位、右补0,即得a=1011 1000。 若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。

6. 右移运算符(>>)

定义: 将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。 例如:a=a>>2 将a的二进制位右移2位,左补0 或者 左补1得看被移数是正还是负。 操作数每右移一位,相当于该数除以2。

7. 原码、补码、反码

上面提到了补码、反码等知识,这里就补充一下。 计算机中的有符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。

(1)原码

原码就是一个数的二进制数。例如:10的原码为0000 1010

(2)反码

  • 正数的反码与原码相同,如:10 反码为 0000 1010
  • 负数的反码为除符号位,按位取反,即0变1,1变0。

例如:-10

原码:1000 1010
反码:1111 0101

(3)补码

  • 正数的补码与原码相同,如:10 补码为 0000 1010
  • 负数的补码是原码除符号位外的所有位取反即0变1,1变0,然后加1,也就是反码加1。

例如:-10

原码:1000 1010
反码:1111 0101
补码:1111 0110

6. escape、encodeURI、encodeURIComponent 的区别

  • encodeURI 是对整个 URI 进行转义,将 URI 中的非法字符转换为合法字符,所以对于一些在 URI 中有特殊意义的字符不会进行转义。
  • encodeURIComponent 是对 URI 的组成部分进行转义,所以一些特殊字符也会得到转义。
  • escape 和 encodeURI 的作用相同,不过它们对于 unicode 编码为 0xff 之外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式,再在每个字节前加上 %。

7. 常见的DOM操作有哪些

1)DOM 节点的获取

DOM 节点的获取的API及使用:

getElementById // 按照 id 查询
getElementsByTagName // 按照标签名查询
getElementsByClassName // 按照类名查询
querySelectorAll // 按照 css 选择器查询

// 按照 id 查询
var imooc = document.getElementById('imooc') // 查询到 id 为 imooc 的元素
// 按照标签名查询
var pList = document.getElementsByTagName('p')  // 查询到标签为 p 的集合
// 按照类名查询
var moocList = document.getElementsByClassName('mooc') // 查询到类名为 mooc 的集合
// 按照 css 选择器查询
var pList = document.querySelectorAll('.mooc') // 查询到类名为 mooc 的集合

2)DOM 节点的创建

创建一个新节点,并把它添加到指定节点的后面。 已知的 HTML 结构如下:

<html>
  <head>
    <title>DEMO</title>
  </head>
  <body>
    <div id="container"> 
      <h1 id="title">我是标题</h1>
    </div>   
  </body>
</html>

要求添加一个有内容的 span 节点到 id 为 title 的节点后面,做法就是:

// 首先获取父节点
var container = document.getElementById('container')
// 创建新节点
var targetSpan = document.createElement('span')
// 设置 span 节点的内容
targetSpan.innerHTML = 'hello world'
// 把新创建的元素塞进父节点里去
container.appendChild(targetSpan)

3)DOM 节点的删除

删除指定的 DOM 节点, 已知的 HTML 结构如下:

<html>
  <head>
    <title>DEMO</title>
  </head>
  <body>
    <div id="container"> 
      <h1 id="title">我是标题</h1>
    </div>   
  </body>
</html>

需要删除 id 为 title 的元素,做法是:

// 获取目标元素的父元素
var container = document.getElementById('container')
// 获取目标元素
var targetNode = document.getElementById('title')
// 删除目标元素
container.removeChild(targetNode)

或者通过子节点数组来完成删除:

// 获取目标元素的父元素
var container = document.getElementById('container')
// 获取目标元素var targetNode = container.childNodes[1]// 删除目标元素container.removeChild(targetNode)

4)修改 DOM 元素

修改 DOM 元素这个动作可以分很多维度,比如说移动 DOM 元素的位置,修改 DOM 元素的属性等。

将指定的两个 DOM 元素交换位置, 已知的 HTML 结构如下:

<html>
  <head>
    <title>DEMO</title>
  </head>
  <body>
    <div id="container"> 
      <h1 id="title">我是标题</h1>
      <p id="content">我是内容</p>
    </div>   
  </body>
</html>

现在需要调换 title 和 content 的位置,可以考虑 insertBefore 或者 appendChild:

// 获取父元素
var container = document.getElementById('container')   
 
// 获取两个需要被交换的元素
var title = document.getElementById('title')
var content = document.getElementById('content')
// 交换两个元素,把 content 置于 title 前面
container.insertBefore(content, title)

8. use strict是什么意思 ? 使用它区别是什么?

use strict 是一种 严格模式,这种模式使得 Javascript 在更严格的条件下运行。设立严格模式的目的如下:

  • 消除 Javascript 语法的不合理、不严谨之处;
  • 消除代码运行的不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;

区别:

  • 禁止使用 with 语句。

  • 禁止 this 关键字指向全局对象。

  • 对象不能有重名的属性。

9. 什么是尾调用,使用尾调用有什么好处?

尾调用指的是函数的最后一步调用另一个函数。代码执行是基于执行栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存。但是 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

10. 如何使用for...of遍历对象

for…of是作为ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,普通的对象用for..of遍历是会报错的。

如果需要遍历的对象是类数组对象,用Array.from转成数组即可。

var obj = {
    0:'one',
    1:'two',
    length: 2
};
obj = Array.from(obj);
for(var k of obj){
    console.log(k)
}

如果不是类数组对象,就给对象添加一个[Symbol.iterator]属性,并指向一个迭代器即可。

//方法一:
var obj = {
    a:1,
    b:2,
    c:3
};

obj[Symbol.iterator] = function(){
	var keys = Object.keys(this);
	var count = 0;
	return {
		next(){
			if(count<keys.length){
				return {value: obj[keys[count++]],done:false};
			}else{
				return {value:undefined,done:true};
			}
		}
	}
};

for(var k of obj){
	console.log(k);
}


// 方法二
var obj = {
    a:1,
    b:2,
    c:3
};
obj[Symbol.iterator] = function*(){
    var keys = Object.keys(obj);
    for(var k of keys){
        yield [k,obj[k]]
    }
};

for(var [k,v] of obj){
    console.log(k,v);
}

11. ajax、axios、fetch的区别

(1)AJAX 传统的网页采用同步更新,触发一个 HTTP 请求,会刷新整个页面。Ajax 使网页实现异步更新。可以在不重新加载整个网页的情况下,实现局部更新。

(2)Fetch 是基于Promise来实现的,返回一个promise对象,在接收到响应后,可以使用响应的方法(如 json()、text()、arrayBuffer())来解析响应数据。在下一个 .then() 方法中处理解析后的数据。

fetch的优点:

  • 采用了.then 链式调用的方式处理结果,利于代码的可读,而且也解决了回调地狱的问题。
  • 支持跨域请求:Fetch API 内置了跨域请求的支持

fetch的缺点:

  • 默认不携带 cookie:手动设置才能携带 cookie fetch(url, {credentials: 'include'})。

  • 无法取消请求:Fetch API 并不支持直接取消请求,需要开发者自己处理取消请求的逻辑。

  • 对于状态码为 4xx(客户端错误)或 5xx(服务器错误)的 HTTP 响应,fetch 方法不会将响应标记为错误或导致 Promise 进入 rejected 状态。

(3)Axios Axios 是一种基于Promise封装的HTTP客户端,它比 .fetch() 具有的另一个特性是执行 JSON 数据时进行自动转换。其特点如下:

  • 浏览器端发起XMLHttpRequests请求

  • node端发起http请求

  • 支持Promise API

  • 监听请求和返回

  • 对请求和返回进行转化

  • 取消请求

  • 自动转换json数据

  • 客户端支持抵御XSRF攻击

四级

1. JavaScript有哪些内置对象

标准内置对象的分类:

(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。例如 Infinity、NaN、undefined、null 字面量

(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。例如 eval()、parseFloat()、parseInt() 等

(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。例如 Object、Function、Boolean、Symbol、Error 等

(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。例如 Number、Math、Date

(5)字符串,用来表示和操作字符串的对象。例如 String、RegExp

(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array

(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。 例如 Map、Set、WeakMap、WeakSet

(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。 例如 SIMD 等

(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。例如 JSON 等

(10)控制抽象对象 例如 Promise、Generator 等

(11)反射。例如 Reflect、Proxy

(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。例如 Intl、Intl.Collator 等

(13)WebAssembly

(14)其他。例如 arguments

总结: js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。

2. 强类型语言和弱类型语言的区别

  • 强类型语言:强类型语言也称为强类型定义语言,是一种总是强制类型定义的语言,要求变量的使用要严格符合定义,所有变量都必须先定义后使用。一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。
  • 弱类型语言:弱类型语言也称为弱类型定义语言,比如JavaScript是弱类型定义的,在JavaScript中就可以将字符串'12'和整数3进行连接得到字符串'123',在相加的时候会进行强制类型转换。

两者对比:强类型语言在速度上可能略逊色于弱类型语言,但是强类型语言带来的严谨性可以有效地帮助避免许多错误。

3. 解释性语言和编译型语言的区别

(1)解释型语言 使用专门的解释器对源程序逐行解释成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。解释型语言不需要事先编译,其直接将源代码解释成机器码并立即执行,所以只要某一平台提供了相应的解释器即可运行该程序。其特点总结如下

  • 解释型语言每次运行都需要将源代码解释称机器码并执行,效率较低;
  • 只要平台提供相应的解释器,就可以运行源代码,所以可以方便源程序移植;
  • JavaScript、Python等属于解释型语言。

(2)编译型语言 使用专门的编译器,针对特定的平台,将高级语言源代码一次性的编译成可被该平台硬件执行的机器码,并包装成该平台所能识别的可执行性程序的格式。在编译型语言写的程序执行之前,需要一个专门的编译过程,把源代码编译成机器语言的文件,如exe格式的文件,以后要再运行时,直接使用编译结果即可,如直接运行exe文件。因为只需编译一次,以后运行时不需要编译,所以编译型语言执行效率高。其特点总结如下:

  • 一次性的编译成平台相关的机器语言文件,运行时脱离开发环境,运行效率高;
  • 与特定平台相关,一般无法移植到其他平台;
  • C、C++等属于编译型语言。

两者主要区别在于: 前者源程序编译后即可在该平台运行,后者是在运行期间才编译。所以前者运行速度快,后者跨平台性好。

JS数据类型

一级

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

基本数据类型:number  boolean  string  null  undefined  Symbol  Biglnt

  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。

  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

引用数据类型 :Object

 

区别:

  • 基本数据类型存储在栈中,占据空间小,大小固定

  • 引用数据类型存储在堆中,占据空间大,大小不固定。引用数据类型在栈中存储地址,当数据被访问时,会首先在栈中找到地址,取得地址后从堆中获取数据。

栈: 栈会自动分配内存空间,会自动释放,大小固定,里面存放是基本类型的值和引用类型的地址

        特点: 自动分配内存空间(先进后出,后进先出)

        存储数据叫: 压栈 

        读取数据叫: 弹栈

 

    堆: 动态分配的内存,大小不固定 不会自动释放,存放引用类型

        特点: 动态分配内存空间,不会自动释放

        注意: 浏览器在读取到Object类型并不会直接执行,而是将会以字符串的形式存储在浏览器的内存中,在栈空间中记录这16进制的地址;

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

(1)typeof

console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof []);              // object    
console.log(typeof function(){});    // function
console.log(typeof {});              // object
console.log(typeof undefined);       // undefined
console.log(typeof null);            // object

其中数组、对象、null都会被判断为object,其他判断都正确。

(2)instanceof

instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型

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

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 来判断数据类型:

var a = Object.prototype.toString;
 
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));

同样是检测对象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. 判断数组的方式有哪些

  • 通过Object.prototype.toString.call()做判断
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
  • 通过原型链做判断
obj.__proto__ === Array.prototype;
  • 通过ES6的Array.isArray()做判断
Array.isArrray(obj);
  • 通过instanceof做判断
obj instanceof Array
  • 通过Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)

4. null和undefined区别

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

undefined 代表的含义是未定义,null 代表的含义是空对象

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

5. intanceof 操作符的实现原理及实现

instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

function myInstanceof(left, right) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(left)
  // 获取构造函数的 prototype 对象
  let prototype = right.prototype; 
 
  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
    proto = Object.getPrototypeOf(proto);
  }
}
   instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
        实现步骤:
        首先获取类型的原型
        然后获得对象的原型
        然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null

        function myinstanctof(left,right){
            console.log(left.constructor===right.prototype.constructor,'left__proto__');
            let leftP=left.constructor
            console.log(leftP);//ƒ Array() { [native code] }
            console.log(right.prototype.constructor);
            if(leftP===right){
                return true
            }
            return false
        }
        console.log(myinstanctof([],Array));

6. 为什么0.1+0.2 ! == 0.3,如何让其相等

计算机是通过二进制的方式存储数据的,0.1 和 0.2 在二进制下是无限循环的小数,因此在计算时会出现精度丢失,导致结果不精确。

toFixed(num) 方法可把 Number 四舍五入为指定小数位数的数字

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

  • 使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。

  • 使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。

  • 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 为false,两个 NaN 为true。

二级

1. typeof null 的结果是什么,为什么?

typeof null 的结果是Object。

这是因为在 JavaScript 的早期版本中,null 被错误地认为是 object 类型。这个错误一直保留了下来,以确保向后兼容性。

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

isNaN 和 Number.isNaN 都是 JavaScript 中用于检查一个值是否为 NaN 的函数

isNaN 函数在检查之前会尝试将参数转换为数字,因此如果参数是非数字类型的,它会尝试将其转换为数字。这就导致了一些意想不到的结果,例如对于非数字的字符串或对象,isNaN 函数会返回 true,这可能不是我们想要的结果。

isNaN("hello"); // true

isNaN(undefined); // true

  Number.isNaN 函数是 ES6 新增的方法,它不会尝试将参数转换为数字,而是严格检查参数是否为 NaN。这意味着只有当参数的值是 NaN 时,Number.isNaN 才会返回 true;对于其他任何值,都会返回 false。

Number.isNaN("hello"); // false
Number.isNaN(undefined); // false

  因此,Number.isNaN 更加严格和可靠,它不会将非数字的值误判为 NaN。在大多数情况下,推荐使用 Number.isNaN 来检查一个值是否为 NaN。

3. 其他值到字符串的转换规则?

  • Null 和 Undefined 类型 ,null 转换为 "null",undefined 转换为 "undefined",
  • Boolean 类型,true 转换为 "true",false 转换为 "false"。
  • Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
  • Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。
  • 对象类型,调用 toString()(Object.prototype.toString())来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。

4. 其他值到数字值的转换规则?

  • Undefined 类型的值转换为 NaN。
  • Null 类型的值转换为 0。
  • Boolean 类型的值,true 转换为 1,false 转换为 0。
  • String 类型如果包含非数字值则转换为 NaN,空字符串为 0。
  • Symbol 类型的值不能转换为数字,会报错。
  • 对象(包括数组)会首先被转换为相应的基本类型值,在进行对应的转换
  • 为0的 null 空字符串 false

5. 其他值到布尔类型的值的转换规则?

以下这些是假值: • undefined • null • +0、-0 和 NaN • ""

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

ToPrimitive方法,每个值隐式自带的方法,将值 (无论是基本类型值还是对象)转换为基本类型值。如果值为基本类型,则直接返回值本身;如果值为对象,其看起来大概是这样:

/**
* @obj 需要转换的对象
* @type 期望的结果类型
*/
ToPrimitive(obj,type)

type的值为number或者string

(1)当typenumber时规则如下:

  • 调用objvalueOf方法,如果为原始值,则返回,否则下一步;
  • 调用objtoString方法,后续同上;
  • 抛出TypeError 异常。

(2)当typestring时规则如下:

  • 调用objtoString方法,如果为原始值,则返回,否则下一步;
  • 调用objvalueOf方法,后续同上;
  • 抛出TypeError 异常。

可以看出两者的主要区别在于调用toStringvalueOf的先后顺序。默认情况下:

  • 如果对象为 Date 对象,则type默认为string
  • 其他情况下,type默认为number

总结上面的规则,对于 Date 以外的对象,转换为基本类型的大概规则可以概括为一个函数:

var objToNumber = value => Number(value.valueOf().toString())
objToNumber([]) === 0
objToNumber({}) === NaN

而 JavaScript 中的隐式类型转换主要发生在+、-、*、/以及==、>、<这些运算符之间。而这些运算符只能操作基本类型值,所以在进行这些运算前的第一步就是将两边的值用ToPrimitive转换成基本类型,再进行操作。

  1. +操作符 +操作符的两边有至少一个string类型变量时,两边的变量都会被隐式转换为字符串;其他情况下两边的变量都会被转换为数字。
1 + '23' // '123'
 1 + false // 1 
 1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
 '1' + false // '1false'
 false + true // 1
  1. -*、``操作符

NaN也是一个数字

1 * '23' // 23
 1 * false // 0
 1 / 'aa' // NaN
  1. 对于==操作符

操作符两边的值都尽量转成number

3 == true // false, 3 转为number为3true转为number为1
'0' == false //true, '0'转为number为0false转为number为0
'0' == 0 // '0'转为number为0
  1. 对于<>比较符

如果两边都是字符串,则比较字母表顺序:

'ca' < 'bd' // false
'a' < 'b' // true

其他情况下,转换为数字再比较:

'12' < 13 // true
false > -1 // true

以上说的是基本类型的隐式转换,而对象会被ToPrimitive转换为基本类型再进行转换:

var a = {}
a > 2 // false

其对比过程如下:

a.valueOf() // {}, 上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]",现在是一个字符串了
Number(a.toString()) // NaN,根据上面 < 和 > 操作符的规则,要转换成数字
NaN > 2 //false,得出比较结果

又比如:

var a = {name:'Jack'}
var b = {age: 18}
a + b // "[object Object][object Object]"

运算过程如下:

a.valueOf() // {},上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]"
b.valueOf() // 同理
b.toString() // "[object Object]"
a + b // "[object Object][object Object]"

三级

1. 如何获取安全的 undefined 值?

因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。表达式 void ___ 没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值。因此可以用 void 0 来获得 undefined。

2. typeof NaN 的结果是什么?

typeof NaN 的结果是Number。

NaN 指“不是一个数字”,NaN “执行数学运算没有成功,这是失败后返回的结果”。

NaN 是一个特殊值,它和自身不相等,是唯一一个与自身不相等的值。NaN !== NaN 为 true。

3. || 和 && 操作符的返回值?

|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先强制转换为布尔类型,然后再执行条件判断。

  • 对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
  • && 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。

|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果

4. + 操作符什么时候用于字符串的拼接?

简单来说就是,如果 + 的其中一个操作数是字符串,则执行字符串拼接,否则执行数字加法。

那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字。

四级

1.什么是JavaScript 的包装类型

在操作基本数据类型时,JavaScript 会自动将其转换成相应的对象类型。

 String 类型

 Number 类型

 Boolean 类型

2. 为什么会有BigInt的提案?

JavaScript中Number.MAX_SAFE_INTEGER表示最⼤安全数字,计算结果是9007199254740991,即在这个数范围内不会出现精度丢失(⼩数除外)。但是⼀旦超过这个范围,js就会出现计算不准确的情况,这在⼤数计算的时候不得不依靠⼀些第三⽅库进⾏解决,因此官⽅提出了BigInt来解决此问题。

3.object.assign和扩展运算法是深拷贝还是浅拷贝,两者区别

  • Object.assign()方法接收的第一个参数作为目标对象,后面的所有参数作为源对象。然后把所有的源对象合并到目标对象中。它会修改了一个对象,因此会触发 ES6 setter。
  • 扩展操作符(…)使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中。它不复制继承的属性或类的属性,但是它会复制ES6的 symbols 属性。

Object.assign和扩展运算符都是浅拷贝。

浅拷贝是指只拷贝对象的第一层属性,对于对象的内部嵌套对象或数组,则会共享引用,而不是创建新的引用。

示例代码:

// 浅拷贝示例
const obj1 = { a: 1, b: { c: 2 } };

// 使用Object.assign进行浅拷贝
const obj2 = Object.assign({}, obj1);
obj2.b.c = 3;

console.log(obj1); // 输出: { a: 1, b: { c: 3 } }

// 使用扩展运算符进行浅拷贝
const obj3 = { ...obj1 };
obj3.b.c = 4;

console.log(obj1); // 输出: { a: 1, b: { c: 4 } }

可以看到,浅拷贝只会复制对象的第一层属性,对于嵌套的对象或数组,仍然是共享同一个引用。因此,修改拷贝后的对象的嵌套属性会影响原始对象。

ES6

一级

1.let、const、var的区别

  • var 和let 用于声明变量,而 const 用于声明常量
  • var 声明的变量不存在块级作用域,而let 和const 存在块级作用域
  • Var存在变量提升,而let 和const不存在变量提升
  • Var 可以重复声明,而let和const 不可以重复声明
  • const 在声明时必须为其赋值

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

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

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

(2)箭头函数没有自己的this

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

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

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

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

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

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

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

(7)箭头函数没有prototype

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

二级

1. const对象的属性可以修改吗

const保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。

基本类型的数据,其值就保存在变量指向的那个内存地址,因此等同于常量。

引用类型的数据,变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针是固定不变的。

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

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

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

  1. 创建一个对象
  2. 将构造函数的作用域赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性)
  3. 指向构造函数中的代码,构造函数中的this指向该对象(也就是为这个对象添加属性和方法)
  4. 返回新的对象

所以,上面的第二、三步,箭头函数都是没有办法执行的。

3. 箭头函数的this指向哪⾥?

箭头函数并没有属于⾃⼰的this,它所谓的this是继承外层的this,作为⾃⼰的 this 值

4. 对 rest 参数的理解

接受函数的多余参数

它还可以把一个分离的参数序列整合成一个数组

function mutiple(...args) {
  let result = 1;
  for (var val of args) {
    result *= val;
  }
  return result;
}
mutiple(1, 2, 3, 4) // 24

这里,传入 mutiple 的是四个分离的参数,但是如果在 mutiple 函数里尝试输出 args 的值,会发现它是一个数组:

function mutiple(...args) {
  console.log(args)
}
mutiple(1, 2, 3, 4) // [1, 2, 3, 4]

5. ES6中模板语法与字符串处理

模板字符串的关键优势有两个:

  • 在模板字符串中,空格、缩进、换行都会被保留
  • 模板字符串完全支持“运算”式的表达式,可以在${}里完成一些计算

ES6中还新增了一系列的字符串方法用于提升开发效率:

(1)存在性判定:在过去,当判断一个字符/字符串是否在某字符串中时,只能用 indexOf > -1 来做。现在 ES6 提供了三个方法:includes、startsWith、endsWith,它们都会返回一个布尔值来告诉你是否存在。

  • includes:判断字符串与子串的包含关系:
const son = 'haha' 
const father = 'xixi haha hehe'
father.includes(son) // true
  • startsWith:判断字符串是否以某个/某串字符开头:
const father = 'xixi haha hehe'
father.startsWith('haha') // false
father.startsWith('xixi') // true
  • endsWith:判断字符串是否以某个/某串字符结尾:
const father = 'xixi haha hehe'
  father.endsWith('hehe') // true

(2)自动重复:可以使用 repeat 方法来使同一个字符串输出多次(被连续复制多次):

const sourceCode = 'repeat for 3 times;'
const repeated = sourceCode.repeat(3) 
console.log(repeated) // repeat for 3 times;repeat for 3 times;repeat for 3 times

三级

1. 扩展运算符的作用及使用场景

(1)对象扩展运算符

对象的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中。

let bar = { a: 1, b: 2 };
let baz = { ...bar }; // { a: 1, b: 2 }

需要注意:扩展运算符对对象实例的拷贝属于浅拷贝

(2)数组扩展运算符

数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。

console.log(...[1, 2, 3])
// 1 2 3
console.log(...[1, [2, 3, 4], 5])
// 1 [2, 3, 4] 5

下面是数组的扩展运算符的应用:

  • 将数组转换为参数序列
function add(x, y) {
  return x + y;
}
const numbers = [1, 2];
add(...numbers) // 3
  • 复制数组
const arr1 = [1, 2];
const arr2 = [...arr1];

要记住:扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中,这里参数对象是个数组,数组里面的所有对象都是基础数据类型,将所有基础数据类型重新拷贝到新的数组中。

  • 合并数组

如果想在数组内合并数组,可以这样:

const arr1 = ['two', 'three'];
const arr2 = ['one', ...arr1, 'four', 'five'];// ["one", "two", "three", "four", "five"]
  • 扩展运算符与解构赋值结合起来,用于生成数组
const [first, ...rest] = [1, 2, 3, 4, 5];first // 1  rest  // [2, 3, 4, 5]

需要注意:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

const [...rest, last] = [1, 2, 3, 4, 5];         // 报错
const [first, ...rest, last] = [1, 2, 3, 4, 5];  // 报错
  • 将字符串转为真正的数组
[...'hello']    // [ "h", "e", "l", "l", "o" ]
  • 任何 Iterator 接口的对象,都可以用扩展运算符转为真正的数组

比较常见的应用是可以将某些数据结构转为数组:

// arguments对象
function foo() {
  const args = [...arguments];
}

用于替换es5中的Array.prototype.slice.call(arguments)写法。

  • 使用Math函数获取数组中特定的值
const numbers = [9, 4, 7, 1];
Math.min(...numbers); // 1
Math.max(...numbers); // 9

2. 对对象与数组的解构的理解

解构是一种新的提取数据的模式,这种模式能够从对象或数组里有针对性地拿到想要的数值。

1)数组的解构 在解构数组时,以元素的位置为匹配条件来提取想要的数据的:

const [a, b, c] = [1, 2, 3]

最终,a、b、c分别被赋予了数组第0、1、2个索引位的值:

const [a,,c] = [1,2,3]

通过把中间位留空,可以顺利地把数组第一位和最后一位的值赋给 a、c 两个变量:

2)对象的解构 在解构对象时,是以属性的名称为匹配条件,来提取想要的数据的。现在定义一个对象:

const stu = {
  name: 'Bob',
  age: 24
}

假如想要解构它的两个自有属性,可以这样:

const { name, age } = stu

这样就得到了 name 和 age 两个和 stu 平级的变量:

四级

1. 如何提取高度嵌套的对象里的指定属性?

const school = {
   classes: {
      stu: {
         name: 'Bob',
         age: 24,
      }
   }
}
const { classes: { stu: { name } }} = school
       
console.log(name)  // 'Bob'

可以在解构出来的变量名右侧,通过冒号+{目标属性名}这种形式,进一步解构它,一直解构到拿到目标数据为止。

原型

一级

1. 对原型、原型链的理解

使用构造函数来新建一个对象的,构造函数的内部都有一个 prototype 属性,包含该构造函数的实例所共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部包含一个指针,指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。

通过Object.getPrototypeOf() 方法来获取对象的原型。

当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,就是原型链的概念。

2. 原型链指向

image.png

二级

1. 原型修改、重写

function Person(name) {
    this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
// 重写原型
Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // false

可以看到修改原型的时候p的构造函数不是指向Person了,因为直接给Person的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数Object,所以这时候p.constructor === Object ,而不是p.constructor === Person。要想成立,就要用constructor指回来:

Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
p.constructor = Person
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // true

三级

1. 原型链的终点是什么?如何打印出原型链的终点?

由于Object是构造函数,原型链终点是Object.prototype.__proto__,而Object.prototype.__proto__=== null // true,所以,原型链的终点是null。原型链上的所有原型都是对象,所有的对象最终都是由Object构造的,而Object.prototype的下一级是Object.prototype.__proto__

2. 如何获得对象非原型链上的属性?

使用后hasOwnProperty()方法来判断属性是否属于原型链的属性:

function iterate(obj){
   var res=[];
   for(var key in obj){
        if(obj.hasOwnProperty(key))
           res.push(key+': '+obj[key]);
   }
   return res;
} 

异步编程

一级

1. 对Promise的理解

Promise 对象是异步编程的一种解决方案,Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。

解决了回调地狱的问题,使异步代码的可读性,可维护性提高

(1)Promise的实例有三个状态:

  • Pending(进行中)
  • Resolved(已完成)
  • Rejected(已拒绝)
  • 一旦从进行状态变成为其他状态就永远不能更改状态了。

状态的改变是通过 resolve() 和 reject() 函数来实现的,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。

2.Promise方法

Promise有五个常用的方法:then()、catch()、all()、race()、finally。下面就来看一下这些方法。

  1. then()

当Promise执行的内容符合成功条件时,调用resolve函数,失败就调用reject函数。Promise创建完了,那该如何调用呢?

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。 then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

当要写有顺序的异步事件时,需要串行时,可以这样写:

let promise = new Promise((resolve,reject)=>{
    ajax('first').success(function(res){
        resolve(res);
    })
})
promise.then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    
})

那当要写的事件没有顺序或者关系时,还如何写呢?可以使用all 方法来解决。

2. catch()

Promise对象除了有then方法,还有一个catch方法,该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。

p.then((data) => {
     console.log('resolved',data);
},(err) => {
     console.log('rejected',err);
     }
); 
p.then((data) => {
    console.log('resolved',data);
}).catch((err) => {
    console.log('rejected',err);
});

3. all()

接收一个由多个promise对象组成的数组,当所有的promise都成功时,执行resolved,有一个失败时,执行rejected

all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected

javascript
let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(1);
	},2000)
});
let promise2 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(2);
	},1000)
});
let promise3 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(3);
	},3000)
});
Promise.all([promise1,promise2,promise3]).then(res=>{
    console.log(res);
    //结果为:[1,2,3] 
})

调用all方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个promise对象resolve执行时的值。

(4)race() 接收一个由多个promise对象组成的数组,当最先执行完的事件执行完之后,就直接返回该promise对象的值,第一个的状态是什么自身的状态也就是什么

race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected

let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       reject(1);
	},2000)
});
let promise2 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(2);
	},1000)
});
let promise3 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(3);
	},3000)
});
Promise.race([promise1,promise2,promise3]).then(res=>{
	console.log(res);
	//结果:2
},rej=>{
    console.log(rej)};
)

那么race方法有什么实际作用呢?当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})

5. finally()

finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。

server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);

finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。finally本质上是then方法的特例:

promise
.finally(() => {
  // 语句
});
// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。

3. 对async/await 的理解

async/await其实是Generator 的语法糖,它是为优化then链而开发出来的。async为“异步”,await则为等待,async 用于申明一个异步的函数,而 await 用于等待一个异步方法执行完成。await只能出现在asnyc函数中

async function testAsy(){
   return 'hello world';
}
let result = testAsy(); 
console.log(result)

async 函数返回的是一个 Promise 对象。如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

async 函数返回的是一个 Promise 对象,用原来的方式:then() 链来处理这个 Promise 对象,就像这样:

async function testAsy(){
   return 'hello world'
}
let result = testAsy() 
console.log(result)
result.then(v=>{
    console.log(v)   // hello world
})

那如果 async 函数没有返回值,返回 Promise.resolve(undefined)

注意: Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

4. async/await对比Promise的优势

  • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
  • Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
  • 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余
  • 调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。

二级

1. 异步编程的实现方式?

  • 回调函数 的方式,使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,不利于代码的可维护。
  • Promise 的方式,使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
  • generator 的方式,它可以在函数的执行过程中,执行到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。next方法的作用是分阶段执行Generator函数。每次调用next方法,会返回一个对象,表示当前阶段的信息(value属性和done属性)。value属性是yield语句后面表达式的值,表示当前阶段的值;done属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段
  • async 函数 的方式,async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。

2. setTimeout、Promise、Async/Await 的区别

(1)setTimeout

指定时间后执行一些任务

setTimeout(function() { console.log("Hello, World!"); }, 2000);

(2)Promise

Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。

console.log('script start')
let promise1 = new Promise(function (resolve) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
}).then(function () {
    console.log('promise2')
})
setTimeout(function(){
    console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout

当JS主线程执行到Promise对象时:

  • promise1.then() 的回调就是一个 task
  • promise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
  • promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
  • setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况

(3)async/await

async function async1(){
   console.log('async1 start');
    await async2();
    console.log('async1 end')
}
async function async2(){
    console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 输出顺序:script start->async1 start->async2->script end->async1 end

async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

例如:

async function func1() {
    return 1
}
console.log(func1())

func1的运行结果其实就是一个Promise对象。因此也可以使用then来处理后续逻辑。

func1().then(res => {
    console.log(res);  // 30
})

await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。

3. Promise.all和Promise.race的区别的使用场景

(1)Promise.all

接收一个由多个promise对象组成的数组,当所有的promise都成功时,执行resolved,有一个失败时,执行rejected

成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值

需要注意,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,这样当遇到发送多个请求并根据请求顺序获取和使用数据的场景,就可以使用Promise.all来解决。

(2)Promise.race

Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。当要做一件事,超过多长时间就不做了,可以用这个方法来解决:

4. await 到底在等啥?

await 在等待什么呢?

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值,注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果。

await 表达式的运算结果取决于它等的是什么。

  • 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
  • 如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。
function testAsy(x){
   return new Promise(resolve=>{setTimeout(() => {
       resolve(x);
     }, 3000)
    }
   )
}
async function testAwt(){    
  let result =  await testAsy('hello world');
  console.log(result);    // 3秒钟之后出现hello world
  console.log('cuger')   // 3秒钟之后出现cug
}
testAwt();
console.log('cug')  //立即输出cug

这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。await暂停当前async的执行,所以'cug''最先输出,hello world'和‘cuger’是3秒钟后同时出现的。

5. async/await的优势

Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化.then,使得代码看起来更像同步代码。

三级

# 1. async/await 如何捕获异常
async function fn(){
    try{
        let a = await Promise.reject('error')
    }catch(error){
        console.log(error)
    }
}

    throw抛出异常信息,程序也会终止执行

    throw后面跟的是错误提示信息

    new Error() 构造函数创建一个新的 Error 对象。这个对象可以包含有关错误的信息。

image.png

    try ... catch 用于捕获错误信息

    将预估可能发生错误的代码写在try代码段中

    如果try代码段中错误后,会执行catch代码段,并截获到错误信息

    finally 不管是否有错误,都会执行

image.png

debugger

    调试,其实就是打断点

执行上下文/作用域链/闭包

1. 对闭包的理解

闭包是指有权访问另一个函数作用域中变量的函数

闭包具有一些重要的特性:

  • 内部函数可以访问外部函数的变量和参数。

  • 外部函数的作用域在内部函数执行完毕后不会立即销毁,因为内部函数仍然引用着外部函数的作用域链。

  • 闭包可以用来创建私有变量和函数,实现模块化的设计模式。

经典面试题:循环中使用闭包解决 var 定义函数的问题

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}
  • 使用 let 定义 i 了来解决问题
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

2. 对作用域、作用域链的理解

1)全局作用域和函数作用域

(1)全局作用域

  • 不在函数内定义的变量拥有全局作用域
  • 未定义直接赋值的变量为全局作用域
  • 所有window对象的属性拥有全局作用域

(2)函数作用域

  • 函数内部的变量,只能在函数内部进行访问
  • 作用域是分层的,内层作用域可以访问外层作用域,反之不行
2)块级作用域
  • 块级作用域是指变量在特定代码块内访问,由{}包裹

  • let和const声明的变量不会有变量提升,也不可以重复声明,存在块级作用域

作用域链: 在当前作用域中查找所需变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。

作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。

3. 对执行上下文的理解

1. 执行上下文类型

(1)全局执行上下文

一个程序中只有一个全局执行上下文,会创建一个全局对象,设置this指向这个全局对象。

(2)函数执行上下文

存在无数个,当一个函数被调用时,就会为该函数创建一个新的执行上下文。

2. 执行上下文栈
  • 代码执行时,遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,函数调用时,就会创建一个新的函数执行上下文并压入栈顶,执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
let a = 'Hello World!';
function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}
function second() {
  console.log('Inside second function');
}
first();
//执行顺序
//先执行second(),在执行first()

简单来说执行上下文就是指:

在执行JS代码之前,先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。

在一个函数执行之前,也会创建一个函数执行上下文环境,指定this指向、arguments和函数的参数。

  • 全局上下文:变量定义,函数声明
  • 函数上下文:变量定义,函数声明,thisarguments

this/call/apply/bind

1. 对this对象的理解

this 指向最后一次调用这个方法的对象,具体值取决于代码被调用的方式。

  • 全局上下文中的this
    • 在浏览器环境中,这个全局对象通常是window对象。
  • 函数上下文中的this
    • 当函数作为全局函数被调用时,this将指向全局对象。
    • 当函数作为对象的方法被调用时,this将指向调用该方法的对象。
    • 当函数使用new关键字被调用时,this将执行新创建的对象。
    • 当使用apply()call()bind()方法来调用函数时,指定this的值。
    • apply 方法接收两个参数:一个是 this 的指向,一个是参数数组。
    • call 方法接收多个参数,第一个是 this 的指向,后面的其余参数是传入函数执行的参数。
    • bind 方法接受多个参数,第一个是 this 的指向,后面的其余参数是传入函数执行的参数,返回一个 this 绑定了的新函数。
    • apply call 会立即调用函数
    • bind 不会立即调用函数

2. 实现call、apply 及 bind 函数

面向对象

对象创建的方式

  1. 对象字面量: 使用花括号 {} 可以直接创建一个对象,并在其中声明属性和方法。例如:

    const obj = {
      property1: value1,
      property2: value2,
      method() {
        // 方法的定义
      }
    };
    
  2. 构造函数: 使用构造函数创建对象是一种常见的方式。通过 new 关键字实例化对象。例如:

    function Person(name, age) {
      this.name = name;
      this.age = age;
      this.sayHello = function() {
        console.log("Hello, I'm " + this.name);
      };
    }
    
    const person1 = new Person("Alice", 25);
    
  3. Object.create() 方法: 使用 Object.create() 方法可以创建一个新对象,将其原型设置为指定的对象。可以在新对象上添加和修改属性。例如:

    const obj1 = {
      property1: value1,
      method1() {
        // 方法的定义
      }
    };
    
    const obj2 = Object.create(obj1);
    obj2.property2 = value2;
    
  4. 使用 class 关键字: ES6 引入了 class 关键字,允许使用类来创建对象。类可以包含构造函数、方法和属性。例如:

    class Car {
      constructor(brand, year) {
        this.brand = brand;
        this.year = year;
      }
    
      accelerate() {
        // 方法的定义
      }
    }
    
    const car1 = new Car("Toyota", 2022);
    

2. 对象继承的方式有哪些?

1.原型链继承

  优点:父类方法可以复用

  缺点: 

  1. 父类所有的引用类型数据(对象,数组)会被子类共享,更改一个子类的数据,其他数据会受到影响,一直变化

  2. 子类实例不能给父类构造函数传参    image.png

2.构造函数继承

优点:父类的应用类型的数据不会被子类共享,不会相互影响

 缺点: 子类不能访问父类原型属性(Person.prototype)上的方法和参数

image.png

3.组合继承

优点: 

1. 父类可以复用

2. 父类构造函数中的应用属性数据不会被共享

缺点:

会调用两次父类的构造函数,会有两份一样属性和方法,会影响性能

image.png

4.寄生组合继承

优点:高效率只调用一次父构造函数,并且因此避免了在子原型上面创建不必渲染,多余的属性。与此同时,原型链还能保持不变

缺点:代码复杂

image.png

5.原型继承

image.png

6.Object.create继承

优点:不需要单独创建构造函数

缺点:属性中包含的引用始终会在相关对象间共享,子类实例不能向父类传参

image.png

7.寄生式继承

image.png

js中几种继承方式

原型链继承:

优点:父类方法可复用 
缺点:
    1.父类所有的引用数据会被子类共享,更改一个子类数据其他数据会受影响;
    2.子类实例不能给父类构造函数传参
    

构造函数继承:

优点:父类的应用类型的数据不会被子类共享,不会互相影响
缺点:子类不能访问父类原型属性上的方法和参数
    

组合继承:

原型链继承和构造函数继承组合在一起
优点:父类可复用;父类构造函数中的应用属性数据不会被共享
缺点:会调用两次父类的构造函数,影响性能

寄生组合继承:

优点:高效率只调用一次父构造函数
缺点:代码复杂
    

原型继承:

Object.create继承:

优点:不需要单独创建构造函数
缺点:属性中包含的引用始终会在相关对象间共享,子类实例不能向父类传参

垃圾回收与内存泄漏

一级

1. 浏览器的垃圾回收机制

(1)垃圾回收的概念

垃圾回收:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收

回收机制

  • Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。

JavaScript中存在两种变量:局部变量和全局变量  

全局变量一般不会回收

一般情况下局部变量的值,不用了,会被自动回收掉

垃圾回收的方式

1.引用计数(旧,已经不在使用)

具体算法,跟踪记录被引用的次数,如果被引用了一次,那么就记录次数1,多次引用会累加++,如果减少一个应用就减1,如果引用的次数是0,则释放内存

引用计数法存在致命的问题:嵌套引用问题

如果两个对象相互引用,尽管他们已不再使用,垃圾回收器不会进行回收,导致内存泄漏

image.png

2. 标记清除法

不再使用的对象”定义为“无法达到根部的对象”,在js全局对象出发定时扫描内存中的对象。凡是能从根部到达的对象,都是还需要使用的。那些无法由根部出发触及到的对象被标记为不在使用,稍后进行回收。


减少垃圾回收

·  对数组进行优化: 在清空一个数组时,可以将数组的长度设置为0

·  对object进行优化: 对于不再使用的对象,就将其设置为null

. 避免循环引用: 避免对象之间的相互引用,使用弱引用或者手动打破循环引用

. 避免不必要的全局变量:全局变量会一直存在于内存中,直到应用程序结束,减少内存的占用

二级

1. 哪些情况会导致内存泄漏

以下四种情况会造成内存的泄漏:

  • 意外的全局变量: 由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
  • 被遗忘的计时器或回调函数: 设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
  • 脱离 DOM 的引用: 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。
  • 闭包: 不合理的使用闭包,从而导致某些变量一直被留在内存当中。

其他

Javascript本地存储的方式有哪些?区别及应用场景?

  1. localStorage:键值对的存储方式,存储大小5MB,长期持久性存储,在不同标签页和窗口之间共享。不会被发送到服务器。
  2. sessionStorage:键值对的存储方式,存储大小5MB,浏览器关闭窗口后自动清除,不会被发送到服务器。
  3. cookie:键值对的存储方式,存储大小4KB,cookie 会在每个 HTTP 请求中自动发送到服务器,可以用于在客户端和服务器之间传递数据,使用场景:用户登录信息的验证。

如何理解JS的异步?

JS是一门单线程的语言,JS运行在浏览器的渲染主线程中,而渲染主线程只有一个,采用同步的方式会导致主线程造成阻塞,当线程中有异步任务时,主线程将任务交给其他线程来进行处理,继续执行后续的代码,当其他线程完成时,加入到消息队列的末尾,等待主线程的调度。

JS的事件循环

JS是一门单线程的语言,JS运行在浏览器的渲染主线程中,在源码中,相当于开启一个不会结束的for循环,每次循环从消息队列中取出第一个任务执行,其他线程只需要在合适的时候将任务加入到队列的末尾即可,根据任务的不同类型加入到不同的任务队列中,主要包括微任务队列,交互任务队列,延时任务队列,网络任务队列,并且微任务的优先级是最高的,必须优先调度执行。

进程和线程

进程是资源分配的最小单位,线程是CPU调度的最小单位

同一进程的多个线程中,若一个线程死掉,则整个进程崩溃;

进程间执行相互独立,一个进程崩溃不影响其他进程运行

  • 1.浏览器有哪些进程和线程 主要有三个进程:

  • 浏览器进程

主要负责界面显示、用户交互

  • 网络进程

负责加载网络资源

  • 渲染进程

渲染进程启动后,会开启一个渲染主线程,主线程负责执行HTML、CSS、JS代码。

默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同的标签页之间不相互影响

并发与并行的区别?

  • 并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。
  • 并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。

iterator

迭代器(iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署iterator接口,就可以完成便遍历操作。 iterator接口主要供for ... of使用

迭代器iterator工作原理

a. 创建一个指针对象,指向当前数据结构的起始位置。

b. 第一次调用对象的next方法,指针自动指向数据结构的第一个成员。

c. 接下来不断的调用next方法,指针一直往后移动,直到指向最后一个成员。

d. 每调用next方法返回一个包含valuedone属性的对象,当value的值为undefined时,done的值为true,调用终止。

举个栗子🌰 for of iterator 实现原理

 obj ={
        name:"张三",
        age:[66,11,12,3,12,1,2231,3,2,123,1,3,2],
        //这里的Symbol.iterator 就代表着内置的 iterator方法
        [Symbol.iterator](){
           let index = 0
            return {
                next:()=>{
                    if(index < this.age.length){
                        index++
                        //这里的value就代表着每次返回的结果
                        return {values:this.age[index-1],done:false}
                    }else{
                        //一直到循环遍历完
                        return {values:undefined,done:true}
                    }
                }
            }
        }
    }
    	// 声明变量接受每次所return出来的值
        let item = obj[Symbol.iterator]()
        console.log(item.next()) //第一次打印结果 66
        console.log(item.next()) //以此类推
        一直到最后返回结果 undefined done true结束 

运行结果:

可以清楚看到,当指针对象指向最后一个元素的时候,返回{values: undefined, done: true}

image.png

Generator 生成器

Generator 函数也是一种异步编程解决方案

Generator 函数是一个遍历器对象生成函数,执行 Generator 函数会返回一个遍历器对象,该对象通过调用next()方法依次遍历 Generator 函数内部的每一个状态。

写法

  • function 关键字与函数名之间有一个*,如:function* myGenerator() { ... }
  • 使用yield 表达式来定义不同的内部状态
//创建一个 Generator 函数 
function* myGenerator() { 
//定义 三个状态 hello,world 和 return 语句
yield "hello"; 
yield "world"; 
return "ending"; 
}
// 调用 Generator 函数,返回一个指向内部状态的指针对象 
var myGen = myGenerator();
//第一次调用next方法:函数头部 => 第一个yield 
//返回一个对象: value = hello(当前yield的状态值) done = false(表示遍历还没结束) 
myGen.next(); // { value: 'hello', done: false } 
//第二次调用next方法:上次停下的地方 => 第二个yield 
//返回一个对象: value = world(当前yield的状态值) done = false(表示遍历还没结束) myGen.next(); // { value: 'world', done: false } 
//第三次调用next方法:上次停下的地方 => return语句 
//返回一个对象: value = ending(return语句后的值) done = true(表示遍历结束) myGen.next(); // { value: 'ending', done: true } 
//第四次调用,此时 Generator 函数已经运行完毕。 
//next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。 myGen.next(); // { value: undefined, done: true }

yield 表达式是暂停执行的标记,而 next 方法可以恢复执行

-next 方法会返回一个对象,该对象拥有以下两个属性:

  • value 属性表示当前的内部状态的值

  • done 属性是一个布尔值,表示是否遍历结束

  • next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。