说一说你对 JavaScript 的理解

252 阅读14分钟

1、JavaScript 的数据类型有哪些?

  • 基础数据类型

    undefined null number string boolean Symbol

  • 引用数据类型 function object array Date RegExp Symbol是表示一个独一无二的变量,就防止我们的变量命名冲突。

判断数组的类型有哪些?

  1. Array.isArray()  判断
  2. instanceof 判断: 检验构造函数的prototype属性是否出现在对象的原型链中,返回一个布尔值。let a = []; a instanceof Array; //true
  3. constructor判断: 实例的构造函数属性constructor指向构造函数let a = [1,3,4]; a.constructor === Array;//true
  4. Object.prototype.toString.call()  判断let a = [1,2,3]; Object.prototype.toString.call(a) === '[object Array]';//true

image.png

2、原型和原型链

原型

  1. js 在创建对象的时候,都有一个 __proto__内置属性,用于指向创建它的构造函数的原型对象。
  2. 每个对象都会有 __proto__,但是只有函数对象才会有 prototype 属性。
  3. 创建一个 function Person() let children = new Person() , children 有一个 __proto__ 属性,他的构造函数是 Person,构造函数的原型对象是 Person.prototype,children.__proto__指向 Person.prototype
  4. 所有函数对象的 __proto__ 都指向 Function.prototype
    • 构造函数自身的 __pro__ 的答案和 Object.__proto__ 的答案是什么 Function.prototype
    • Object.__prototype 的答案呢?Function.prototype.__proto__Function.__proto.__.__proto__
    // 原型
    console.log(Object.__proto__ === Function.prototype); // true
    console.log(Object.prototype === Function.__proto__.__proto__); // true
    console.log(Object.prototype === Function.prototype.__proto__); // true
  1. 构造函数的 this 永远指向他的实例对象 原型链
  2. 构造函数他自身也可能是另外一个函数的实例,所以说通过 protoprototype,这样链接的方式就形成了原型链
  3. 原型链的尽头是 Object.prototype.__proto__ === null // true
console.log(children.__proto__ === Person.prototype) // true
console.log(String.__proto__ === Function.prototype) // true
console.log(String.construtor === Function) // true
鲨鱼哥出的题
Object.prototype.__proto__` 原型链的尽头是什么? null
console.log(Function.prototype.__proto__ === Function.__proto__.__proto__);
Function.prototype.__proto__ 答案是什么 Function.__proto__.__proto__
构造函数自身的 `__proto__` 是什么?`Object.prototype
console.log(Object instanceof Function) // true
console.log(Function.prototype === Function.__proto__) //true

// 手写代码合集
console.log(Object.__proto__ === Function.prototype); //true
console.log(Function.__proto__.__proto__ === Object.prototype); //true
console.log(Function.prototype.__proto__ === Object.prototype); //true
console.log(Function.__proto__ === Function.prototype); //true
console.log(Object.prototype.__proto__); //null 原型链的尽头

3、JS 继承的几种方式?

【ES6】class继承(整理篇)

一万字ES6的class类,再学不懂,请把我锤死(语法篇)

// 原型链继承
function Person() {
  this.name = "Zaxlct"; //Person.prototype.name = "Zaxlct";
}
// 一样的写法
Person.prototype.sayName = function () {
  alert(this.name);
};
let person1 = new Person();
let person2 = new Person();
person1.name = 666;
console.log(person1.name); //666
console.log(person2.name); //Zaxlct
// 原型继承:将子类的原型对象指向父类的实例
// 优点:继承了父类的模板和父类的原型对象
// 缺点:无法实现多继承,无法传参。
function Parent() {
  this.name = "小三";
}
function Child() {
  this.age = 21;
}
Child.prototype = new Parent();
let obj1 = new Child();
let obj2 = new Parent();

obj1.name = "小舞";

console.log(obj1); // Child {age: 21, name: '小舞'}
console.log(obj2); // Parent {name: '小三'}
// 构造函数继承:在子类构造函数内用 apply、call 来改变父类构造函数的 this 指向。
// 优点:可以实现多继承,可以继承了父类的实例的属性和方法,也可以传参。
// 缺点: 不能继承父类的原型属性和方法。
function Parent(name, age) {
  this.name = name;
  this.age = age;
  this.aaa = 666;
}
function Child(name, age) {
  Parent.call(this, name, age);
}

let obj = new Child("小三", 21);
console.log(obj);
console.log(obj.name, obj.age, obj.aaa); // 小三  21  666
// 组合继承:将原型链继承和构造函数函数继承组合在一起。
// 原型链继承是为了保证子类能够继承父类的原型属性和方法
// 构造函数继承是为了保证子类能够继承父类的实例实行和方法
function Parent(name) {
  this.name = name;
  this.color = [1, 2, 3];
}

function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

Child.prototype = new Parent();

var child1 = new Child("小三", 21);
var child2 = new Child("小舞", 22);

child1.color.push(4);

console.log(child1); // Child {name: '小三', color: Array(4), age: 21}
console.log(child2); // Child {name: '小三', color: Array(3), age: 21}

Object.setPrototypeOf 与 Object.create区别?

setPrototypeOf 与 Object.create区别

使用Object.create,Animal.prototype将会指向一个空对象,空对象的原型属性指向Plants的prototytpe。所以我们不能再访问Animal的原有prototypoe中的属性。Object.create的使用方式也凸显了直接重新赋值。

使用Object.setPrototypeOf则会将Animal.prototype将会指向Animal原有的prototype,然后这个prototype的prototype再指向Plants的prototytpe。所以我们优先访问的Animal,然后再是plants。

在进行俩个原型之间的委托时使用setPrototype更好,Object.create更适和直接对一个无原生原型的对象快速进行委托。

总结

1、 一个创造对象,一个修改原型

2、Object.create 是创建参数为 __proto__ 的对象,Object.setPrototype 是以修改参数为 __proto__ 的对象

4、typeof 和instanceof 的区别

  1. typeof 是判断数据类型的,会返回变量的基本类型。
  2. instanceof 是检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,返回的是布尔值。
  3. 可以准确的判断复杂引用数据类型,但是不能正确判断基础数据类型。
  4. Object === Function 通过 Object.__proto__ === Function.prototype 判断
  5. Function === Object 通过 Function.prototype.__proto__ === Object.__proto__ 判断

5、常见的 dom 操作有哪些?

  1. 创建节点: createElement
  2. 查询节点 getElementById querySelectAll getElementsByClassName
  3. 更新节点: innerHTML
  4. 添加节点: innerHMTL appenChild
  5. 删除节点: removeChild
<!-- getElementsByClassName -->
<div class="example">1</div>
<div class="example">2</div>
<button onclick="myFunction()">点我</button>
<script>
  function myFunction() {
    var x = document.getElementsByClassName("example");
    x[0].innerHTML = "Hello World!";
  }
</script>
<!-- querySelectorAll -->
<h2 class="example">1</h2>
<p class="example">2</p>
<button onclick="myFunction()">点我</button>
<script>
  function myFunction() {
    var x = document.querySelectorAll(".example");
    x[0].style.backgroundColor = "red";
  }
</script>


//getElementsByTagName
<script>
<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  <li>item n</li>
</ul>
// 获取目标元素
const lis = document.getElementsByTagName("li")
// 循环遍历绑定事件
for (let i = 0; i < lis.length; i++) {
    lis[i].onclick = function(e){
        console.log(e.target.innerHTML)
    }
}
</script>

6、说一下闭包吧?

  1. 闭包就是函数内嵌套函数,或者子函数在外调用,子函数在父函数的作用域内不会被释放。
  2. 内部的函数可以访问外部函数的参数和变量
  3. 闭包实现了函数的封装缓存,但是消耗内存,可以在退出函数前,将不必要的参数变量删除。
  4. 应用场景有防抖和节流函数柯里化
  • 函数柯里化:通过函数调用,继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式。

    优点: 参数复用 提高实用性

function add () {
    const args = [...arguments]
    function fn () {
        args.push(...arguments)
        return fn
    }
    fn.toString = function () {
        return args.reduce((sum, i) => {
            return sm + i
        })
    }
    return fn
}
add(1)(2)(3)(4)  // 10

7、防抖和节流

  • 节流 :在 N 秒内只执行一次的任务,如果在 N 秒内被重复重发,只执行一次。
  • 防抖 :在 N 秒后执行的任务,如果在 N 秒内被重复重发,则重新计时。
    // 防抖
    function debounce(fn, delay = 300) {
      //默认300毫秒
      let timer;
      return function () {
        const args = arguments;
        if (timer) {
          clearTimeout(timer);
        }
        timer = setTimeout(() => {
          fn.apply(this, args); // 改变this指向为调用debounce所指的对象
        }, delay);
      };
    }

    window.addEventListener(
      "scroll",
      debounce(() => {
        console.log(111);
      }, 1000)
    );

    // 节流
    // 设置一个标志
    function throttle(fn, delay) {
      let flag = true;
      return () => {
        if (!flag) return;
        flag = false;
        timer = setTimeout(() => {
          fn();
          flag = true;
        }, delay);
      };
    }

    window.addEventListener(
      "scroll",
      throttle(() => {
        console.log(111);
      }, 1000)
    );

防抖在连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  • 手机号、邮箱验证输入检测
  • 窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

节流在间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 搜索框,搜索联想功能

8、 Event Loop 事件循环

事件循环 是为了解决单线程阻塞问题的,<Script> 代码分为同步任务和异步任务,异步任务又分为宏任务和微任务。

  1. 同步任务指的是:在主线程上排队执行的任务,只有前一个任务执行结束,后一个任务才开始执行。
  2. 异步任务指的是:不进入主线程,而进入任务队列的任务、只有等主线程任务执行完毕,任务队列的任务才会进入主线程执行。
  3. 当某个宏任务执行完,会查看是否有微任务队列。如果有,先执行微任务队列中的所有任务;如果没有,就会读取宏任务队列中排在最前的任务;
  4. 执行宏任务的过程中,遇到微任务,依次加入微任务队列。栈空后,再次读取微任务队列里的任务,以此类推就是事件循环。
  5. 同步任务(Promise)>异步任务-微任务(Promise.then Promise.catch) > 异步任务-宏任务(settimeout)

9、事件模型、事件绑定、事件监听、事件捕获、事件冒泡、事件委托

事件流分为三个阶段:事件捕获阶段、目标阶段、事件冒泡阶段

  • 事件捕获阶段:从最外层 window 向内查找目标元素,查找的过程中不会处理响应元素注册的冒泡事件
  • 目标阶段:触发事件最底层的元素
  • 冒泡阶段:触发事件从最底层开始向外一层一层的传递到 window 层

在冒泡中,内部元素先被触发,然后再触发外部元素 捕获中,外部元素先被触发,在触发内部元素

原生事件绑定是通过 document.getElementById('btn1') 获取到节点,然后通过 addEventListener 完成事件绑定。

event.stopPropagation() // 阻止冒泡

event.stopImmediatePropagation() // 阻止捕获

event.preventDefault() // 阻止发生默认行为

应用场景是

事件委托

  1. 事件委托的原理: 就是不给每个子节点单独设置事件监听器,而是设置在父节点上,然后利用事件冒泡原理设置每个子节点。
  2. 优点: 减少了内存消耗和 dom 操作,提高性能,防止重排和重绘,还有就是动态绑定事件,事件绑定在父节点上,新增的元素也能触发同样的事件。
  3. 事件委托的事件有: click mousedown mouseup keydown keyup
  4. focus() blur() 没有事件冒泡机制,无法进行事件委托。
  5. mousemove mouseout 需要计算定位,消耗比较高,也不适合事件委托 如果把所有事件都用事件代理,可能会出现事件误判,即本不该被触发的事件被绑定上了事件
<body>
  <ul id="list">
    <li>item 1</li>
    <li>item 2</li>
    <li>item 3</li>
    <li>item 4</li>
  </ul>
  <script>
    // var a = document.getElementById('a')
    // var b = document.getElementById('b')
    // var c = document.getElementById('c')
    // document.getElementsByClassName
    // document.getElementsByName
    //注册捕获事件监听器
    // a.addEventListener('click', () => { console.log("冒泡a") })
    // b.addEventListener('click', () => { console.log('冒泡b') })
    // c.addEventListener('click', () => { console.log("捕获c") }, true)
    // c.addEventListener('click', () => { console.log("冒泡c") })
    // a.addEventListener('click', () => { console.log("捕获a") }, true)
    // b.addEventListener('click', () => { console.log('捕获b') }, true)
    let list = document.getElementById('list')
    list.addEventListener('click', function (e) {
      if (e.target.innerHTML === 'item 1') {
        console.log('阻止事件冒泡了');
        e.stopPropagation()
      } else {
        console.log(e.target.innerHTML);
      }
    }, false)
  </script>
</body>

10、箭头函数和普通函数的区别?

  1. 箭头函数的语法比普通函数的语法更加接简洁,但是不能使用 argumentsupernew.targer,不能用做构造函数,因为箭头函数没有 prototype
  2. 箭头函数的 this 指向它的外层环境,外层对象。而且是在创建时判断的,不是在运行时判断的
  3. 普通函数的 this 指向调用它的对象

普通函数可以调用 apply call bind 三者改变 this 的指向

最搞笑的一点就是曾经面试的时候背问过

  • 箭头函数可不可以调用 apply call 呢? 自己大声的回答: 不可以 丢脸丢到家了 其实是可以的,因为 apply call shi ES5 的属性
  • 面试官又接着问 会改变 this 的指向吗? 我的回答 可以 ,哎,对自己无语了 其实是不可以的

11、谈谈 this 的理解

函数的调用方式决定了 this 的值

  1. 函数调用:非严格模式下 this 是指向全局对象的,严格模式下是 undefined
  2. 方法调用:this 指向调用函数的对象
  3. 构造函数调用:this 指向 new 创建的实例对象
  4. call、apply :是指第一个参数 this 的指向、

12、bind apply call 三者的区别?

JavaScript 中 call()、apply()、bind() 的用法

  1. 三者都能够改变 this 的指向。
  2. 三者的第一个参数都是 this 指向的那个对象,如果 或者是 undefinednull 则默认指向 window
  3. apply 传的数数组,call,bind 传的是参数列表
  4. bind 是返回一个绑定 this 之后的函数,稍后执行。 apply call 是立即执行。
  5. bind 返回的新函数如果用做构造函数创建新对象,此时 this 不再指向 bind 的第一参数,而是指向 new 创建的实例。

13、this 的指向

14、 new 操作符都干了什么?

  1. new 创建了一个 obj
  2. 将 obj 和构造函数通过原型链连接在一起,obj 可以访问构造函数和原型链上的属性和方法
  3. 将构造函数的 this 绑定在 obj 上
  4. 返回一个对象,该函数没有返回对象的话,则返回 this
  5. 构造函数的 this 永远指向它的实例对象

new 操作符新建了一个空对象,这个对象原型指向构造函数的 prototype,执行构造函数后返回这个对象。

    // 手写 new
    function mynew(fn, ...args) {
      let obj = Object.create(fn.prototype)
      let res = fn.call(obj, ...args)
      if (res && (typeof res === 'object' || typeof res === 'function')) {
        return res
      }
      return obj
    }
    // 使用
    function Person(name, age) {
      this.name = name
      this.age = age
    }
    Person.prototype.play = function () {
      console.log(this.age);
    }
    let p1 = mynew(Person, '小王', 21)
    console.log(p1); // {name: "小王", age: 21}
    console.log(p1.name); //小王
    p1.play() // 21

1. new操作符的实现原理

new操作符的执行过程:

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

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

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

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

15、说一下深拷贝和浅拷贝

浅拷贝:以赋值的方式拷贝一个新对象,仍指向同一个地址,修改时原对象也会随之改变。

有三种方法:赋值的方式 =Object.assign解构赋值 ...

// 赋值的方式 =
function depp(obj) {
  let newobj = {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      newobj[key] = obj[key];
    }
  }
  return newobj;
}
let obj1 = deep(obj);
// Object.assign
let obj1 = Object.assign({}, obj);
// 解构赋值的方式
let obj1 = [...obj];

深拷贝:完全拷贝一个新对象,修改时原对象不会发生改变。

有两种方法进行拷贝: JSON.parse(JSON.stringify(obj))递归循环

// 深拷贝:新拷贝一个对象,修改时原对象不会改变
// 1. JSON.parse(JSON.stringify(Obj))
// 2. 递归循环拷贝
//
let obj = {
  name: "小三",
  unfind: undefined,
  fun: function () {},
  symbol: Symbol("唯一值"),
};
//
let obj1 = JSON.parse(JSON.stringify(obj));
// 缺点就是 undfined,Symbol,function不会进行拷贝,所以要用递归循环
//
function deepOjb(obj, hash = new WeakMap()) {
  if (obj === null) return obj; //如果是 null undefined 的话就不拷贝
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  if (obj !== "object") return obj; //如果是普通类型的值的话就不进行拷贝
  if (hash.get(obj)) return hash.get(obj);
  let newobj = new obj.constructor(); //把拷贝对象的构造函数指向此创建的对象
  hash.set(obj, newobj);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 进行递归拷贝
      newobj[key] = deepObj(obj[key], hash);
    }
  }
}

16、 0.1 + 0.2 为什么不等于 0.3

因为 JS 有一个存储浮点数精度丢失的问题,计算机计算是把十进制转换成二进制计算的,0.1 和 0.2 转换成二进制后有很多位数。计算结果是 0.30000000000000004

可以通过 (0.1 * 10 + 0.2 * 10) / 10 === 0.3

17、作用域和作用域链

作用域有:全局作用域,函数作用域,块级作用域。块级作用域是 ES6 新增的。

  1. 全局作用域:在 <script>包裹区域内,也是最外层函数包裹区域内。全局变量过多会引起命名冲突。
  2. 函数作用域:是在函数内部,内层的作用域可以访问外层的作用域,反之不行。
  3. 块级作用域:就是 let const 定义的区域内,有 {} 包裹着的

作用域链:查找变量的时候会从当前上下文查找变量对象,没有找到,就去父级变量对象上查找,一直找到全局变量对象。由多个上下文变量对象构成的链表就是作用域链。

18、 == === Object.is 的区别

  1. == 是值相等类型不等的话,他会自动帮你做类型转换,然后输出 true。
  2. === 是类型和值都必须相等才会输出 true ,不会帮你做类型转换。
  3. Object 唯一的区别就是除了 NaN 和 -0 和 +0
consolo({} === {}) // false
consolo([] === []) // false

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

let a = [1]
let b = {}
console.log(typeof b);  // object
console.log(typeof null);  // object
console.log(typeof a);  // object

19、map 和 forEach 的区别?

  1. map 会返回全新的数组,forEach 不会,返回一个 undefined
  2. 需要创建新数组的时候使用 map,不需要创建使用

for...in 和 for...of 的区别

for...in:可以遍历对象,数组,输出的是 key for...of:不能遍历对象,输出的是 value

  1. for…of 是 ES6 新增的遍历方式,允许遍历一个含有 iterator 接口的数据结构(数组、对象等)并且返回各项的值,和 ES3 中的 for…in 的区别如下
  2. for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
  3. for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链; 对于数组的遍历
  • for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性)
  • for…of 只返回数组的下标对应的属性值;

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

怎么判断一个对象是否为空?

Object.keys(obj).length === 0 // true 则为空对象

杂项

typeof NaN 的结果是什么?指的不是一个数字

console.log(typeof NaN); // number
console.log(NaN !== NaN); // true

== 的转换规则 string 转换成 number boolean 转换成 number

|| 和 && 的规则 || 为 true 的话返回第一个,false 的话返回第二个 && 为 true 的话返回第二个,false 的话返回第一个

console.log(0 || 2) // 2
console.log(1 || 2) // 1
console.log(1 && 2) // 2

undefined 和 null 的区别?

undefined 表示未定义的值

  1. 声明了一个变量,但是没有被赋值
  2. 访问对象上不存在的属性或者未定义的变量
  3. 函数定义了形参,没有传实参

null 表示空值