面试 你必需要懂的原生JS知识点

上篇
html

1. 基本类型有哪几种?null 是对象吗?基本数据类型和复杂数据类型存储有什么区别?

  • 基本类型有6种,分别是undefined,null,bool,string,number,symbol(ES6新增)。
  • 虽然 typeof null 返回的值是 object,可是null不是对象,而是基本数据类型的一种。
  • 基本数据类型存储在栈内存,存储的是值。
  • 复杂数据类型存储在堆内存,存储的是地址。当咱们把对象赋值给另一个变量的时候,复制的是地址,指向同一块内存空间,当其中一个对象改变时,另外一个对象也会变化。

2. typeof 是否正确判断类型? instanceof呢? instanceof 的实现原理是什么?

首先 typeof 可以正确的判断基本数据类型,可是除了 null, typeof null输出的是对象。前端

可是对象来讲,typeof 不能正确的判断其类型, typeof 一个函数能够输出 'function',而除此以外,输出的全是 object,这种状况下,咱们没法准确的知道对象的类型。node

instanceof能够准确的判断复杂数据类型,可是不能正确判断基本数据类型。nginx

instanceof 是经过原型链判断的,A instanceof B, 在A的原型链中层层查找,是否有原型等于B.prototype,若是一直找到A的原型链的顶端(null;即Object.prototype.__proto__),仍然不等于B.prototype,那么返回false,不然返回true.git

instanceof的实现代码:github

// L instanceof R
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
    var O = R.prototype;// 取 R 的显式原型
    L = L.__proto__;    // 取 L 的隐式原型
    while (true) { 
        if (L === null) //已经找到顶层
            return false;  
        if (O === L)   //当 O 严格等于 L 时,返回 true
            return true; 
        L = L.__proto__;  //继续向上一层原型链查找
    } 
}复制代码

3. for of , for in 和 forEach,map 的区别。

  • for...of循环:具备 iterator 接口,就能够用for...of循环遍历它的成员(属性值)。for...of循环可使用的范围包括数组、Set 和 Map 结构、某些相似数组的对象、Generator 对象,以及字符串。for...of循环调用遍历器接口,数组的遍历器接口只返回具备数字索引的属性。对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。能够中断循环。
  • for...in循环:遍历对象自身的和继承的可枚举的
    属性
    , 不能直接获取属性值。能够中断循环。
  • forEach: 只能遍历数组,不能中断,没有返回值(或认为返回值是undefined)。
  • map: 只能遍历数组,不能中断,返回值是修改后的数组。

PS: Object.keys():返回给定对象全部可枚举web

属性的字符串数组

关于forEach是否会改变原数组的问题,有些小伙伴提出了异议,为此我写了代码测试了下(注意数组项是复杂数据类型的状况)。 除了forEach以外,map等API,也有一样的问题。面试

let arry = [1, 2, 3, 4];

arry.forEach((item) => {
    item *= 10;
});
console.log(arry); //[1, 2, 3, 4]

arry.forEach((item) => {
    arry[1] = 10; //直接操做数组
});
console.log(arry); //[ 1, 10, 3, 4 ]

let arry2 = [
    { name: "Yve" },
    { age: 20 }
];
arry2.forEach((item) => {
    item.name = 10;
});
console.log(arry2);//[ { name: 10 }, { age: 20, name: 10 } ]复制代码


4. 如何判断一个变量是否是数组?

  • 使用 Array.isArray 判断,若是返回 true, 说明是数组
  • 使用 instanceof Array 判断,若是返回true, 说明是数组
  • 使用 Object.prototype.toString.call 判断,若是值是 [object Array], 说明是数组
  • 经过 constructor 来判断,若是是数组,那么 arr.constructor === Array. (不许确,由于咱们能够指定 obj.constructor = Array)
function fn() {
    console.log(Array.isArray(arguments));   //false; 由于arguments是类数组,但不是数组
    console.log(Array.isArray([1,2,3,4]));   //true
    console.log(arguments instanceof Array); //fasle
    console.log([1,2,3,4] instanceof Array); //true
    console.log(Object.prototype.toString.call(arguments)); //[object Arguments]
    console.log(Object.prototype.toString.call([1,2,3,4])); //[object Array]
    console.log(arguments.constructor === Array); //false
    arguments.constructor = Array;
    console.log(arguments.constructor === Array); //true
    console.log(Array.isArray(arguments));        //false
}
fn(1,2,3,4);复制代码

5. 类数组和数组的区别是什么?

类数组:express

1)拥有length属性,其它属性(索引)为非负整数(对象中的索引会被当作字符串来处理);编程

2)不具备数组所具备的方法;

类数组是一个普通对象,而真实的数组是Array类型。

常见的类数组有: 函数的参数 arugments, DOM 对象列表(好比经过 document.querySelectorAll 获得的列表), jQuery 对象 (好比 $("div")).

类数组能够转换为数组:

//第一种方法
Array.prototype.slice.call(arrayLike, start);
//第二种方法
[...arrayLike];
//第三种方法:
Array.from(arrayLike);复制代码

PS: 任何定义了遍历器(Iterator)接口的对象,均可以用扩展运算符转为真正的数组。

Array.from方法用于将两类对象转为真正的数组:相似数组的对象(array-like object)和可遍历(iterable)的对象。

6. == 和 === 有什么区别?

=== 不须要进行类型转换,只有类型相同而且值相等时,才返回 true.

== 若是二者类型不一样,首先须要进行类型转换。具体流程以下:

  1. 首先判断二者类型是否相同,若是相等,判断值是否相等.
  2. 若是类型不一样,进行类型转换
  3. 判断比较的是不是 null 或者是 undefined, 若是是, 返回 true .
  4. 判断二者类型是否为 string 和 number, 若是是, 将字符串转换成 number
  5. 判断其中一方是否为 boolean, 若是是, 将 boolean 转为 number 再进行判断
  6. 判断其中一方是否为 object 且另外一方为 string、number 或者 symbol , 若是是, 将 object 转为原始类型再进行判断
let person1 = {
    age: 25
}
let person2 = person1;
person2.gae = 20;
console.log(person1 === person2); //true,注意复杂数据类型,比较的是引用地址复制代码

思考: [] == ![]

咱们来分析一下: [] == ![] 是true仍是false?

  1. 首先,咱们须要知道 ! 优先级是高于 == (更多运算符优先级可查看: 运算符优先级)
  2. ![] 引用类型转换成布尔值都是true,所以![]的是false
  3. 根据上面的比较步骤中的第五条,其中一方是 boolean,将 boolean 转为 number 再进行判断,false转换成 number,对应的值是 0.
  4. 根据上面比较步骤中的第六条,有一方是 number,那么将object也转换成Number,空数组转换成数字,对应的值是0.(空数组转换成数字,对应的值是0,若是数组中只有一个数字,那么转成number就是这个数字,其它状况,均为NaN)
  5. 0 == 0; 为true

7. ES6中的class和ES5的类有什么区别?

  1. ES6 class 内部全部定义的方法都是不可枚举的;
  2. ES6 class 必须使用 new 调用;
  3. ES6 class 不存在变量提高;
  4. ES6 class 默认便是严格模式;
  5. ES6 class 子类必须在父类的构造函数中调用super(),这样才有this对象;ES5中类继承的关系是相反的,先有子类的this,而后用父类的方法应用在this上。

8. 数组的哪些API会改变原数组?

修改原数组的API有:

splice/reverse/fill/copyWithin/sort/push/pop/unshift/shift

不修改原数组的API有:

slice/map/forEach/every/filter/reduce/entries/find

注: 数组的每一项是简单数据类型,且未直接操做数组的状况下(稍后会对此题从新做答)。

9. let、const 以及 var 的区别是什么?

  • let 和 const 定义的变量不会出现变量提高,而 var 定义的变量会提高。
  • let 和 const 是JS中的块级做用域
  • let 和 const 不容许重复声明(会抛出错误)
  • let 和 const 定义的变量在定义语句以前,若是使用会抛出错误(造成了暂时性死区),而 var 不会。
  • const 声明一个只读的常量。一旦声明,常量的值就不能改变(若是声明是一个对象,那么不能改变的是对象的引用地址)

10. 在JS中什么是变量提高?什么是暂时性死区?

变量提高就是变量在声明以前就可使用,值为undefined。

在代码块内,使用 let/const 命令声明变量以前,该变量都是不可用的(会抛出错误)。这在语法上,称为“暂时性死区”。暂时性死区也意味着 typeof 再也不是一个百分百安全的操做。

typeof x; // ReferenceError(暂时性死区,抛错)
let x;复制代码
typeof y; // 值是undefined,不会报错复制代码

暂时性死区的本质就是,只要一进入当前做用域,所要使用的变量就已经存在了,可是不可获取,只有等到声明变量的那一行代码出现,才能够获取和使用该变量。

11. 如何正确的判断this? 箭头函数的this是什么?

this的绑定规则有四种:默认绑定,隐式绑定,显式绑定,new绑定.

  1. 函数是否在 new 中调用(new绑定),若是是,那么 this 绑定的是新建立的对象。
  2. 函数是否经过 call,apply 调用,或者使用了 bind (即硬绑定),若是是,那么this绑定的就是指定的对象。
  3. 函数是否在某个上下文对象中调用(隐式绑定),若是是的话,this 绑定的是那个上下文对象。通常是 obj.foo()
  4. 若是以上都不是,那么使用默认绑定。若是在严格模式下,则绑定到 undefined,不然绑定到全局对象。
  5. 若是把 null 或者 undefined 做为 this 的绑定对象传入 call、apply 或者 bind, 这些值在调用时会被忽略,实际应用的是默认绑定规则。
  6. 箭头函数没有本身的 this, 它的this继承于上一层代码块的this。

测试下是否已经成功Get了此知识点(浏览器执行环境):

var number = 5;
var obj = {
    number: 3,
    fn1: (function () {
        var number;
        this.number *= 2;
        number = number * 2;
        number = 3;
        return function () {
            var num = this.number;
            this.number *= 2;
            console.log(num);
            number *= 3;
            console.log(number);
        }
    })()
}
var fn1 = obj.fn1;
fn1.call(null);
obj.fn1();
console.log(window.number);复制代码

若是this的知识点,您还不太懂,请戳: 你真的懂this吗?

12. 词法做用域和this的区别。

  • 词法做用域是由你在写代码时将变量和块做用域写在哪里来决定的
  • this 是在调用时被绑定的,this 指向什么,彻底取决于函数的调用位置(关于this的指向问题,本文已经有说明)

13. 谈谈你对JS执行上下文栈和做用域链的理解。

执行上下文就是当前 JavaScript 代码被解析和执行时所在环境, JS执行上下文栈能够认为是一个存储函数调用的栈结构,遵循先进后出的原则。

  • JavaScript执行在单线程上,全部的代码都是排队执行。
  • 一开始浏览器执行全局的代码时,首先建立全局的执行上下文,压入执行栈的顶部。
  • 每当进入一个函数的执行就会建立函数的执行上下文,而且把它压入执行栈的顶部。当前函数执行-完成后,当前函数的执行上下文出栈,并等待垃圾回收。
  • 浏览器的JS执行引擎老是访问栈顶的执行上下文。
  • 全局上下文只有惟一的一个,它在浏览器关闭时出栈。

做用域链: 不管是 LHS 仍是 RHS 查询,都会在当前的做用域开始查找,若是没有找到,就会向上级做用域继续查找目标标识符,每次上升一个做用域,一直到全局做用域为止。

题难不难?

不难!
继续挑战一下!
难!
知道难,就更要继续了!


14. 什么是闭包?闭包的做用是什么?闭包有哪些使用场景?

闭包是指有权访问另外一个函数做用域中的变量的函数,建立闭包最经常使用的方式就是在一个函数内部建立另外一个函数。

闭包的做用有:

  1. 封装私有变量
  2. 模仿块级做用域(ES5中没有块级做用域)
  3. 实现JS的模块

15. call、apply有什么区别?call,aplly和bind的内部是如何实现的?

call 和 apply 的功能相同,区别在于传参的方式不同:

  • fn.call(obj, arg1, arg2, ...),调用一个函数, 具备一个指定的this值和分别地提供的参数(参数的列表)。

  • fn.apply(obj, [argsArray]),调用一个函数,具备一个指定的this值,以及做为一个数组(或类数组对象)提供的参数。

call核心:

  • 将函数设为传入参数的属性
  • 指定this到函数并传入给定参数执行函数
  • 若是不传入参数或者参数为null,默认指向为 window / global
  • 删除参数上的函数
Function.prototype.call = function (context) {
    /** 若是第一个参数传入的是 null 或者是 undefined, 那么指向this指向 window/global */
    /** 若是第一个参数传入的不是null或者是undefined, 那么必须是一个对象 */
    if (!context) {
        //context为null或者是undefined
        context = typeof window === 'undefined' ? global : window;
    }
    context.fn = this; //this指向的是当前的函数(Function的实例)
    let rest = [...arguments].slice(1);//获取除了this指向对象之外的参数, 空数组slice后返回的仍然是空数组
    let result = context.fn(...rest); //隐式绑定,当前函数的this指向了context.
    delete context.fn;
    return result;
}

//测试代码
var foo = {
    name: 'Selina'
}
var name = 'Chirs';
function bar(job, age) {
    console.log(this.name);
    console.log(job, age);
}
bar.call(foo, 'programmer', 20);
// Selina programmer 20
bar.call(null, 'teacher', 25);
// 浏览器环境: Chirs teacher 25; node 环境: undefined teacher 25
复制代码

apply:

apply的实现和call很相似,可是须要注意他们的参数是不同的,apply的第二个参数是数组或类数组.

Function.prototype.apply = function (context, rest) {
    if (!context) {
        //context为null或者是undefined时,设置默认值
        context = typeof window === 'undefined' ? global : window;
    }
    context.fn = this;
    let result;
    if(rest === undefined || rest === null) {
        //undefined 或者 是 null 不是 Iterator 对象,不能被 ...
        result = context.fn(rest);
    }else if(typeof rest === 'object') {
        result = context.fn(...rest);
    }
    delete context.fn;
    return result;
}
var foo = {
    name: 'Selina'
}
var name = 'Chirs';
function bar(job, age) {
    console.log(this.name);
    console.log(job, age);
}
bar.apply(foo, ['programmer', 20]);
// Selina programmer 20
bar.apply(null, ['teacher', 25]);
// 浏览器环境: Chirs programmer 20; node 环境: undefined teacher 25复制代码

bind

bind 和 call/apply 有一个很重要的区别,一个函数被 call/apply 的时候,会直接调用,可是 bind 会建立一个新函数。当这个新函数被调用时,bind() 的第一个参数将做为它运行时的 this,以后的一序列参数将会在传递的实参前传入做为它的参数。

Function.prototype.bind = function(context) {
    if(typeof this !== "function"){
       throw new TypeError("not a function");
    }
    let self = this;
    let args = [...arguments].slice(1);
    function Fn() {};
    Fn.prototype = this.prototype;
    let bound = function() {
        let res = [...args, ...arguments]; //bind传递的参数和函数调用时传递的参数拼接
        context = this instanceof Fn ? this : context || this;
        return self.apply(context, res);
    }
    //原型链
    bound.prototype = new Fn();
    return bound;
}

var name = 'Jack';
function person(age, job, gender){
    console.log(this.name , age, job, gender);
}
var Yve = {name : 'Yvette'};
let result = person.bind(Yve, 22, 'enginner')('female');	复制代码

16. new的原理是什么?经过new的方式建立对象和经过字面量建立有什么区别?

new:

  1. 建立一个新对象。
  2. 这个新对象会被执行[[原型]]链接。
  3. 将构造函数的做用域赋值给新对象,即this指向这个新对象.
  4. 若是函数没有返回其余对象,那么new表达式中的函数调用会自动返回这个新对象。
function new(func) {
    let target = {};
    target.__proto__ = func.prototype;
    let res = func.call(target);
    if (typeof(res) == "object" || typeof(res) == "function") {
    	return res;
    }
    return target;
}复制代码

字面量建立对象,不会调用 Object构造函数, 简洁且性能更好;

new Object() 方式建立对象本质上是方法调用,涉及到在proto链中遍历该方法,当找到该方法后,又会生产方法调用必须的 堆栈信息,方法调用结束后,还要释放该堆栈,性能不如字面量的方式。

经过对象字面量定义对象时,不会调用Object构造函数。

17. 谈谈你对原型的理解?

在 JavaScript 中,每当定义一个对象(函数也是对象)时候,对象中都会包含一些预约义的属性。其中每一个函数对象都有一个prototype 属性,这个属性指向函数的原型对象。使用原型对象的好处是全部对象实例共享它所包含的属性和方法。

18. 什么是原型链?【原型链解决的是什么问题?】

原型链解决的主要是继承问题。

每一个对象拥有一个原型对象,经过 proto (读音: dunder proto) 指针指向其原型对象,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null(Object.proptotype.__proto__ 指向的是null)。这种关系被称为原型链 (prototype chain),经过原型链一个对象能够拥有定义在其余对象中的属性和方法。

构造函数 Parent、Parent.prototype 和 实例 p 的关系以下:(p.__proto__ === Parent.prototype)


19. prototype 和 __proto__ 区别是什么?

prototype是构造函数的属性。

__proto__ 是每一个实例都有的属性,能够访问 [[prototype]] 属性。

实例的__proto__ 与其构造函数的prototype指向的是同一个对象。

function Student(name) {
    this.name = name;
}
Student.prototype.setAge = function(){
    this.age=20;
}
let Jack = new Student('jack');
console.log(Jack.__proto__);
//console.log(Object.getPrototypeOf(Jack));;
console.log(Student.prototype);
console.log(Jack.__proto__ === Student.prototype);//true复制代码

20. 使用ES5实现一个继承?

组合继承(最经常使用的继承方式)

function SuperType() {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
}

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

SubType.prototype.sayAge = function() {
    console.log(this.age);
}复制代码

其它继承方式实现,能够参考《JavaScript高级程序设计》

21. 什么是深拷贝?深拷贝和浅拷贝有什么区别?

浅拷贝是指只复制第一层对象,可是当对象的属性是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。

深拷贝复制变量值,对于非基本类型的变量,则递归至基本类型变量后,再复制。深拷贝后的对象与原来的对象是彻底隔离的,互不影响,对一个对象的修改并不会影响另外一个对象。

实现一个深拷贝:

function deepClone(obj) { //递归拷贝
    if(obj === null) return null; //null 的状况
    if(obj instanceof RegExp) return new RegExp(obj);
    if(obj instanceof Date) return new Date(obj);
    if(typeof obj !== 'object') {
        //若是不是复杂数据类型,直接返回
        return obj;
    }
    /**
     * 若是obj是数组,那么 obj.constructor 是 [Function: Array]
     * 若是obj是对象,那么 obj.constructor 是 [Function: Object]
     */
    let t = new obj.constructor();
    for(let key in obj) {
        //若是 obj[key] 是复杂数据类型,递归
        t[key] = deepClone(obj[key]);
    }
    return t;
}复制代码

看不下去了?别人的

送分题
会成为你的
送命题


22. 防抖和节流的区别是什么?防抖和节流的实现。

防抖和节流的做用都是防止函数屡次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于设置的时间,防抖的状况下只会调用一次,而节流的状况会每隔必定时间调用一次函数。

防抖(debounce): n秒内函数只会执行一次,若是n秒内高频事件再次被触发,则从新计算时间

function debounce(func, wait, immediate=true) {
    let timeout, context, args;
        // 延迟执行函数
        const later = () => setTimeout(() => {
            // 延迟函数执行完毕,清空定时器
            timeout = null
            // 延迟执行的状况下,函数会在延迟函数中执行
            // 使用到以前缓存的参数和上下文
            if (!immediate) {
                func.apply(context, args);
                context = args = null;
            }
        }, wait);
        let debounced = function (...params) {
            if (!timeout) {
                timeout = later();
                if (immediate) {
                    //当即执行
                    func.apply(this, params);
                } else {
                    //闭包
                    context = this;
                    args = params;
                }
            } else {
                clearTimeout(timeout);
                timeout = later();
            }
        }
    debounced.cancel = function () {
        clearTimeout(timeout);
        timeout = null;
    };
    return debounced;
};复制代码

防抖的应用场景:

  • 每次 resize/scroll 触发统计事件
  • 文本输入的验证(连续输入文字后发送 AJAX 请求进行验证,验证一次就好)

节流(throttle): 高频事件在规定时间内只会执行一次,执行一次后,只有大于设定的执行周期后才会执行第二次。

//underscore.js
function throttle(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function () {
        previous = options.leading === false ? 0 : Date.now() || new Date().getTime();
        timeout = null;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function () {
        var now = Date.now() || new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            result = func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            // 判断是否设置了定时器和 trailing
            timeout = setTimeout(later, remaining);
        }
        return result;
    };

    throttled.cancel = function () {
        clearTimeout(timeout);
        previous = 0;
        timeout = context = args = null;
    };

    return throttled;
};
复制代码

函数节流的应用场景有:

  • DOM 元素的拖拽功能实现(mousemove)
  • 射击游戏的 mousedown/keydown 事件(单位时间只能发射一颗子弹)
  • 计算鼠标移动的距离(mousemove)
  • Canvas 模拟画板功能(mousemove)
  • 搜索联想(keyup)
  • 监听滚动事件判断是否到页面底部自动加载更多:给 scroll 加了 debounce 后,只有用户中止滚动后,才会判断是否到了页面底部;若是是 throttle 的话,只要页面滚动就会间隔一段时间判断一次

23. 取数组的最大值(ES五、ES6)

// ES5 的写法
Math.max.apply(null, [14, 3, 77, 30]);

// ES6 的写法
Math.max(...[14, 3, 77, 30]);

// reduce
[14,3,77,30].reduce((accumulator, currentValue)=>{
    return accumulator = accumulator > currentValue ? accumulator : currentValue
});复制代码

24. ES6新的特性有哪些?

  1. 新增了块级做用域(let,const)
  2. 提供了定义类的语法糖(class)
  3. 新增了一种基本数据类型(Symbol)
  4. 新增了变量的解构赋值
  5. 函数参数容许设置默认值,引入了rest参数,新增了箭头函数
  6. 数组新增了一些API,如 isArray / from / of 方法;数组实例新增了 entries(),keys() 和 values() 等方法
  7. 对象和数组新增了扩展运算符
  8. ES6 新增了模块化(import/export)
  9. ES6 新增了 Set 和 Map 数据结构
  10. ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例
  11. ES6 新增了生成器(Generator)和遍历器(Iterator)

25. setTimeout倒计时为何会出现偏差?

setTimeout() 只是将事件插入了“任务队列”,必须等当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码消耗时间很长,也有可能要等好久,因此并没办法保证回调函数必定会在 setTimeout() 指定的时间执行。因此, setTimeout() 的第二个参数表示的是最少时间,并不是是确切时间。

HTML5标准规定了 setTimeout() 的第二个参数的最小值不得小于4毫秒,若是低于这个值,则默认是4毫秒。在此以前。老版本的浏览器都将最短期设为10毫秒。另外,对于那些DOM的变更(尤为是涉及页面从新渲染的部分),一般是间隔16毫秒执行。这时使用 requestAnimationFrame() 的效果要好于 setTimeout();

26. 为何 0.1 + 0.2 != 0.3 ?

0.1 + 0.2 != 0.3 是由于在进制转换和进阶运算的过程当中出现精度损失。

下面是详细解释:

JavaScript使用 Number 类型表示数字(整数和浮点数),使用64位表示一个数字。


图片说明:

  • 第0位:符号位,0表示正数,1表示负数(s)
  • 第1位到第11位:储存指数部分(e)
  • 第12位到第63位:储存小数部分(即有效数字)f

计算机没法直接对十进制的数字进行运算, 须要先对照 IEEE 754 规范转换成二进制,而后对阶运算。

1.进制转换

0.1和0.2转换成二进制后会无限循环

0.1 -> 0.0001100110011001...(无限循环)
0.2 -> 0.0011001100110011...(无限循环)
复制代码复制代码

可是因为IEEE 754尾数位数限制,须要将后面多余的位截掉,这样在进制之间的转换中精度已经损失。

2.对阶运算

因为指数位数不相同,运算时须要对阶运算 这部分也可能产生精度损失。

按照上面两步运算(包括两步的精度损失),最后的结果是

0.0100110011001100110011001100110011001100110011001100

结果转换成十进制以后就是 0.30000000000000004。

27. promise 有几种状态, Promise 有什么优缺点 ?

promise有三种状态: fulfilled, rejected, pending.

Promise 的优势:

  1. 一旦状态改变,就不会再变,任什么时候候均可以获得这个结果
  2. 能够将异步操做以同步操做的流程表达出来,避免了层层嵌套的回调函数

Promise 的缺点:

  1. 没法取消 Promise
  2. 当处于pending状态时,没法得知目前进展到哪个阶段

28. Promise构造函数是同步仍是异步执行,then中的方法呢 ?promise如何实现then处理 ?

Promise的构造函数是同步执行的。then 中的方法是异步执行的。

promise的then实现,

29. Promise和setTimeout的区别 ?

Promise 是微任务,setTimeout 是宏任务,同一个事件循环中,promise.then老是先于 setTimeout 执行。

30. 如何实现 Promise.all ?

要实现 Promise.all,首先咱们须要知道 Promise.all 的功能:

  1. 若是传入的参数是一个空的可迭代对象,那么此promise对象回调完成(resolve),只有此状况,是同步执行的,其它都是异步返回的。
  2. 若是传入的参数不包含任何 promise,则返回一个异步完成. promises 中全部的promise都“完成”时或参数中不包含 promise 时回调完成。
  3. 若是参数中有一个promise失败,那么Promise.all返回的promise对象失败
  4. 在任何状况下,Promise.all 返回的 promise 的完成状态的结果都是一个数组
Promise.all = function (promises) {
    return new Promise((resolve, reject) => {
        let index = 0;
        let result = [];
        if (promises.length === 0) {
            resolve(result);
        } else {
            function processValue(i, data) {
                result[i] = data;
                if (++index === promises.length) {
                    resolve(result);
                }
            }
            for (let i = 0; i < promises.length; i++) {
                //promises[i] 多是普通值
                Promise.resolve(promises[i]).then((data) => {
                    processValue(i, data);
                }, (err) => {
                    reject(err);
                    return;
                });
            }
        }
    });
}
复制代码复制代码


31.如何实现 Promise.finally ?

无论成功仍是失败,都会走到finally中,而且finally以后,还能够继续then。而且会将值原封不动的传递给后面的then.

Promise.prototype.finally = function (callback) {
    return this.then((value) => {
        return Promise.resolve(callback()).then(() => {
            return value;
        });
    }, (err) => {
        return Promise.resolve(callback()).then(() => {
            throw err;
        });
    });
}
复制代码复制代码

32. 什么是函数柯里化?实现 sum(1)(2)(3) 返回结果是1,2,3之和

函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,而且返回接受余下的参数并且返回结果的新函数的技术。

function sum(a) {
    return function(b) {
        return function(c) {
            return a+b+c;
        }
    }
}
console.log(sum(1)(2)(3)); // 6
复制代码复制代码

引伸:实现一个curry函数,将普通函数进行柯里化:

function curry(fn, args = []) {
    return function(){
        let rest = [...args, ...arguments];
        if (rest.length < fn.length) {
            return curry.call(this,fn,rest);
        }else{
            return fn.apply(this,rest);
        }
    }
}
//test
function sum(a,b,c) {
    return a+b+c;
}
let sumFn = curry(sum);
console.log(sumFn(1)(2)(3)); //6
console.log(sumFn(1)(2, 3)); //6复制代码

本文转载自 前端小姐姐




下篇

1.说一说JS异步发展史

异步最先的解决方案是回调函数,如事件的回调,setInterval/setTimeout中的回调。可是回调函数有一个很常见的问题,就是回调地狱的问题(稍后会举例说明);

为了解决回调地狱的问题,社区提出了Promise解决方案,ES6将其写进了语言标准。Promise解决了回调地狱的问题,可是Promise也存在一些问题,如错误不能被try catch,并且使用Promise的链式调用,其实并无从根本上解决回调地狱的问题,只是换了一种写法。

ES6中引入 Generator 函数,Generator是一种异步编程解决方案,Generator 函数是协程在 ES6 的实现,最大特色就是能够交出函数的执行权,Generator 函数能够看出是异步任务的容器,须要暂停的地方,都用yield语句注明。可是 Generator 使用起来较为复杂。

ES7又提出了新的异步解决方案:async/await,async是 Generator 函数的语法糖,async/await 使得异步代码看起来像同步代码,异步编程发展的目标就是让异步逻辑的代码看起来像同步同样。

1.回调函数: callback

//node读取文件
fs.readFile(xxx, 'utf-8', function(err, data) {
    //code
});复制代码

回调函数的使用场景(包括但不限于):

  1. 事件回调
  2. Node API
  3. setTimeout/setInterval中的回调函数

异步回调嵌套会致使代码难以维护,而且不方便统一处理错误,不能try catch 和 回调地狱(如先读取A文本内容,再根据A文本内容读取B再根据B的内容读取C...)。

fs.readFile(A, 'utf-8', function(err, data) {
    fs.readFile(B, 'utf-8', function(err, data) {
        fs.readFile(C, 'utf-8', function(err, data) {
            fs.readFile(D, 'utf-8', function(err, data) {
                //....
            });
        });
    });
});复制代码

2.Promise

Promise 主要解决了回调地狱的问题,Promise 最先由社区提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

那么咱们看看Promise是如何解决回调地狱问题的,仍然以上文的readFile为例。

function read(url) {
    return new Promise((resolve, reject) => {
        fs.readFile(url, 'utf8', (err, data) => {
            if(err) reject(err);
            resolve(data);
        });
    });
}
read(A).then(data => {
    return read(B);
}).then(data => {
    return read(C);
}).then(data => {
    return read(D);
}).catch(reason => {
    console.log(reason);
});复制代码

想要运行代码看效果,请戳(VS的 Code Runner 执行代码): github.com/YvetteLau/B…

思考一下在Promise以前,你是如何处理异步并发问题的,假设有这样一个需求:读取三个文件内容,都读取成功后,输出最终的结果。有了Promise以后,又如何处理呢?代码可戳: github.com/YvetteLau/B…

注: 可使用 bluebird 将接口 promise化;

引伸: Promise有哪些优势和问题呢?

3.Generator

Generator 函数是 ES6 提供的一种异步编程解决方案,整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操做须要暂停的地方,都用 yield 语句注明。

Generator 函数通常配合 yield 或 Promise 使用。Generator函数返回的是迭代器。对生成器和迭代器不了解的同窗,请自行补习下基础。下面咱们看一下 Generator 的简单使用:

function* gen() {
    let a = yield 111;
    console.log(a);
    let b = yield 222;
    console.log(b);
    let c = yield 333;
    console.log(c);
    let d = yield 444;
    console.log(d);
}
let t = gen();
//next方法能够带一个参数,该参数就会被看成上一个yield表达式的返回值
t.next(1); //第一次调用next函数时,传递的参数无效
t.next(2); //a输出2;
t.next(3); //b输出2; 
t.next(4); //c输出3;
t.next(5); //d输出3;复制代码

为了让你们更好的理解上面代码是如何执行的,我画了一张图,分别对应每一次的next方法调用:


仍然以上文的readFile为例,使用 Generator + co库来实现:

const fs = require('fs');
const co = require('co');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);

function* read() {
    yield readFile(A, 'utf-8');
    yield readFile(B, 'utf-8');
    yield readFile(C, 'utf-8');
    //....
}
co(read()).then(data => {
    //code
}).catch(err => {
    //code
});
复制代码

不使用co库,如何实现?可否本身写一个最简的my_co?请戳: github.com/YvetteLau/B…

PS: 若是你还不太了解 Generator/yield,建议阅读ES6相关文档。

4.async/await

ES7中引入了 async/await 概念。async实际上是一个语法糖,它的实现就是将Generator函数和自动执行器(co),包装在一个函数中。

async/await 的优势是代码清晰,不用像 Promise 写不少 then 链,就能够处理回调地狱的问题。错误能够被try catch。

仍然以上文的readFile为例,使用 Generator + co库来实现:

const fs = require('fs');
const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);


async function read() {
    await readFile(A, 'utf-8');
    await readFile(B, 'utf-8');
    await readFile(C, 'utf-8');
    //code
}

read().then((data) => {
    //code
}).catch(err => {
    //code
});复制代码

可执行代码,请戳:github.com/YvetteLau/B…

思考一下 async/await 如何处理异步并发问题的? github.com/YvetteLau/B…

若是你有更好的答案或想法,欢迎在这题目对应的github下留言:说一说JS异步发展史

2.谈谈对 async/await 的理解,async/await 的实现原理是什么?

async/await 就是 Generator 的语法糖,使得异步操做变得更加方便。来张图对比一下:


async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成await。

咱们说 async 是 Generator 的语法糖,那么这个糖究竟甜在哪呢?

1)async函数内置执行器,函数调用以后,会自动执行,输出最后结果。而Generator须要调用next或者配合co模块使用。

2)更好的语义,async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操做,await表示紧跟在后面的表达式须要等待结果。

3)更广的适用性。co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async 函数的 await 命令后面,能够是 Promise 对象和原始类型的值。

4)返回值是Promise,async函数的返回值是 Promise 对象,Generator的返回值是 Iterator,Promise 对象使用起来更加方便。

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

具体代码试下以下(和spawn的实现略有差别,我的以为这样写更容易理解),若是你想知道如何一步步写出 my_co ,可戳: github.com/YvetteLau/B…

function my_co(it) {
    return new Promise((resolve, reject) => {
        function next(data) {
            try {
                var { value, done } = it.next(data);
            }catch(e){
                return reject(e);
            }
            if (!done) { 
                //donetrue,表示迭代完成
                //value 不必定是 Promise,多是一个普通值。使用 Promise.resolve 进行包装。
                Promise.resolve(value).then(val => {
                    next(val);
                }, reject);
            } else {
                resolve(value);
            }
        }
        next(); //执行一次next
    });
}
function* test() {
    yield new Promise((resolve, reject) => {
        setTimeout(resolve, 100);
    });
    yield new Promise((resolve, reject) => {
        // throw Error(1);
        resolve(10)
    });
    yield 10;
    return 1000;
}

my_co(test()).then(data => {
    console.log(data); //输出1000
}).catch((err) => {
    console.log('err: ', err);
});复制代码

若是你有更好的答案或想法,欢迎在这题目对应的github下留言:谈谈对 async/await 的理解,async/await 的实现原理是什么?

3.使用 async/await 须要注意什么?

  1. await 命令后面的Promise对象,运行结果多是 rejected,此时等同于 async 函数返回的 Promise 对象被reject。所以须要加上错误处理,能够给每一个 await 后的 Promise 增长 catch 方法;也能够将 await 的代码放在 try...catch 中。
  2. 多个await命令后面的异步操做,若是不存在继发关系,最好让它们同时触发。
//下面两种写法均可以同时触发
//法一
async function f1() {
    await Promise.all([
        new Promise((resolve) => {
            setTimeout(resolve, 600);
        }),
        new Promise((resolve) => {
            setTimeout(resolve, 600);
        })
    ])
}
//法二
async function f2() {
    let fn1 = new Promise((resolve) => {
            setTimeout(resolve, 800);
        });
    
    let fn2 = new Promise((resolve) => {
            setTimeout(resolve, 800);
        })
    await fn1;
    await fn2;
}复制代码
  1. await命令只能用在async函数之中,若是用在普通函数,会报错。
  2. async 函数能够保留运行堆栈。
/**
* 函数a内部运行了一个异步任务b()。当b()运行的时候,函数a()不会中断,而是继续执行。
* 等到b()运行结束,可能a()早就* 运行结束了,b()所在的上下文环境已经消失了。
* 若是b()或c()报错,错误堆栈将不包括a()。
*/
function b() {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, 200)
    });
}
function c() {
    throw Error(10);
}
const a = () => {
    b().then(() => c());
};
a();
/**
* 改为async函数
*/
const m = async () => {
    await b();
    c();
};
m();
复制代码

报错信息以下,能够看出 async 函数能够保留运行堆栈。


若是你有更好的答案或想法,欢迎在这题目对应的github下留言:使用 async/await 须要注意什么?

4.如何实现 Promise.race?

在代码实现前,咱们须要先了解 Promise.race 的特色:

  1. Promise.race返回的仍然是一个Promise. 它的状态与第一个完成的Promise的状态相同。它能够是完成( resolves),也能够是失败(rejects),这要取决于第一个Promise是哪种状态。

  2. 若是传入的参数是不可迭代的,那么将会抛出错误。

  3. 若是传的参数数组是空,那么返回的 promise 将永远等待。

  4. 若是迭代包含一个或多个非承诺值和/或已解决/拒绝的承诺,则 Promise.race 将解析为迭代中找到的第一个值。

Promise.race = function (promises) {
    //promises 必须是一个可遍历的数据结构,不然抛错
    return new Promise((resolve, reject) => {
        if (typeof promises[Symbol.iterator] !== 'function') {
            //真实不是这个错误
            Promise.reject('args is not iteratable!');
        }
        if (promises.length === 0) {
            return;
        } else {
            for (let i = 0; i < promises.length; i++) {
                Promise.resolve(promises[i]).then((data) => {
                    resolve(data);
                    return;
                }, (err) => {
                    reject(err);
                    return;
                });
            }
        }
    });
}复制代码

测试代码:

//一直在等待态
Promise.race([]).then((data) => {
    console.log('success ', data);
}, (err) => {
    console.log('err ', err);
});
//抛错
Promise.race().then((data) => {
    console.log('success ', data);
}, (err) => {
    console.log('err ', err);
});
Promise.race([
    new Promise((resolve, reject) => { setTimeout(() => { resolve(100) }, 1000) }),
    new Promise((resolve, reject) => { setTimeout(() => { resolve(200) }, 200) }),
    new Promise((resolve, reject) => { setTimeout(() => { reject(100) }, 100) })
]).then((data) => {
    console.log(data);
}, (err) => {
    console.log(err);
});复制代码

引伸: Promise.all/Promise.reject/Promise.resolve/Promise.prototype.finally/Promise.prototype.catch 的实现原理,若是还不太会,戳:Promise源码实现

若是你有更好的答案或想法,欢迎在这题目对应的github下留言:如何实现 Promise.race?


5.可遍历数据结构的有什么特色?

一个对象若是要具有可被 for...of 循环调用的 Iterator 接口,就必须在其 Symbol.iterator 的属性上部署遍历器生成方法(或者原型链上的对象具备该方法)

PS: 遍历器对象根本特征就是具备next方法。每次调用next方法,都会返回一个表明当前成员的信息对象,具备value和done两个属性。

//如为对象添加Iterator 接口;
let obj = {
    name: "Yvette",
    age: 18,
    job: 'engineer',
    [Symbol.iterator]() {
        const self = this;
        const keys = Object.keys(self);
        let index = 0;
        return {
            next() {
                if (index < keys.length) {
                    return {
                        value: self[keys[index++]],
                        done: false
                    };
                } else {
                    return { value: undefined, done: true };
                }
            }
        };
    }
};

for(let item of obj) {
    console.log(item); //Yvette  18  engineer
}复制代码

使用 Generator 函数(遍历器对象生成函数)简写 Symbol.iterator 方法,能够简写以下:

let obj = {
    name: "Yvette",
    age: 18,
    job: 'engineer',
    * [Symbol.iterator] () {
        const self = this;
        const keys = Object.keys(self);
        for (let index = 0;index < keys.length; index++) {
            yield self[keys[index]];//yield表达式仅能使用在 Generator 函数中
        } 
    }
};复制代码

原生具有 Iterator 接口的数据结构以下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象
  • ES6 的数组、Set、Map 都部署了如下三个方法: entries() / keys() / values(),调用后都返回遍历器对象。

若是你有更好的答案或想法,欢迎在这题目对应的github下留言:可遍历数据结构的有什么特色?

6.requestAnimationFrame 和 setTimeout/setInterval 有什么区别?使用 requestAnimationFrame 有哪些好处?

在 requestAnimationFrame 以前,咱们主要使用 setTimeout/setInterval 来编写JS动画。

编写动画的关键是循环间隔的设置,一方面,循环间隔足够短,动画效果才能显得平滑流畅;另外一方面,循环间隔还要足够长,才能确保浏览器有能力渲染产生的变化。

大部分的电脑显示器的刷新频率是60HZ,也就是每秒钟重绘60次。大多数浏览器都会对重绘操做加以限制,不超过显示器的重绘频率,由于即便超过那个频率用户体验也不会提高。所以,最平滑动画的最佳循环间隔是 1000ms / 60 ,约为16.7ms。

setTimeout/setInterval 有一个显著的缺陷在于时间是不精确的,setTimeout/setInterval 只能保证延时或间隔不小于设定的时间。由于它们实际上只是把任务添加到了任务队列中,可是若是前面的任务尚未执行完成,它们必需要等待。

requestAnimationFrame 才有的是系统时间间隔,保持最佳绘制效率,不会由于间隔时间太短,形成过分绘制,增长开销;也不会由于间隔时间太长,使用动画卡顿不流畅,让各类网页动画效果可以有一个统一的刷新机制,从而节省系统资源,提升系统性能,改善视觉效果。

综上所述,requestAnimationFrame 和 setTimeout/setInterval 在编写动画时相比,优势以下:

1.requestAnimationFrame 不须要设置时间,采用系统时间间隔,能达到最佳的动画效果。

2.requestAnimationFrame 会把每一帧中的全部DOM操做集中起来,在一次重绘或回流中就完成。

3.当 requestAnimationFrame() 运行在后台标签页或者隐藏的 <iframe> 里时,requestAnimationFrame() 会被暂停调用以提高性能和电池寿命(大多数浏览器中)。

requestAnimationFrame 使用(试试使用requestAnimationFrame写一个移动的小球,从A移动到B初):

function step(timestamp) {
    //code...
    window.requestAnimationFrame(step);
}
window.requestAnimationFrame(step);
复制代码

若是你有更好的答案或想法,欢迎在这题目对应的github下留言:requestAnimationFrame 和 setTimeout/setInterval 有什么区别?使用 requestAnimationFrame 有哪些好处?

7.JS 类型转换的规则是什么?

类型转换的规则三言两语说不清,真想哇得一声哭出来~


JS中类型转换分为 强制类型转换 和 隐式类型转换 。

  • 经过 Number()、parseInt()、parseFloat()、toString()、String()、Boolean(),进行强制类型转换。

  • 逻辑运算符(&&、 ||、 !)、运算符(+、-、*、/)、关系操做符(>、 <、 <= 、>=)、相等运算符(==)或者 if/while 的条件,可能会进行隐式类型转换。

强制类型转换

1.Number() 将任意类型的参数转换为数值类型

规则以下:

  • 若是是布尔值,true和false分别被转换为1和0
  • 若是是数字,返回自身
  • 若是是 null,返回 0
  • 若是是 undefined,返回 NAN
  • 若是是字符串,遵循如下规则:
    1. 若是字符串中只包含数字(或者是 0X / 0x 开头的十六进制数字字符串,容许包含正负号),则将其转换为十进制
    2. 若是字符串中包含有效的浮点格式,将其转换为浮点数值
    3. 若是是空字符串,将其转换为0
    4. 如不是以上格式的字符串,均返回 NaN
  • 若是是Symbol,抛出错误
  • 若是是对象,则调用对象的 valueOf() 方法,而后依据前面的规则转换返回的值。若是转换的结果是 NaN ,则调用对象的 toString() 方法,再次依照前面的规则转换返回的字符串值。

部份内置对象调用默认的 valueOf 的行为:

对象 返回值
Array 数组自己(对象类型)
Boolean 布尔值(原始类型)
Date 从 UTC 1970 年 1 月 1 日午夜开始计算,到所封装的日期所通过的毫秒数
Function 函数自己(对象类型)
Number 数字值(原始类型)
Object 对象自己(对象类型)
String 字符串值(原始类型)
Number('0111'); //111
Number('0X11') //17
Number(null); //0
Number(''); //0
Number('1a'); //NaN
Number(-0X11);//-17复制代码

2.parseInt(param, radix)

若是第一个参数传入的是字符串类型:

  1. 忽略字符串前面的空格,直至找到第一个非空字符,若是是空字符串,返回NaN
  2. 若是第一个字符不是数字符号或者正负号,返回NaN
  3. 若是第一个字符是数字/正负号,则继续解析直至字符串解析完毕或者遇到一个非数字符号为止

若是第一个参数传入的Number类型:

  1. 数字若是是0开头,则将其看成八进制来解析(若是是一个八进制数);若是以0x开头,则将其看成十六进制来解析

若是第一个参数是 null 或者是 undefined,或者是一个对象类型:

  1. 返回 NaN

若是第一个参数是数组: 1. 去数组的第一个元素,按照上面的规则进行解析

若是第一个参数是Symbol类型: 1. 抛出错误

若是指定radix参数,以radix为基数进行解析

parseInt('0111'); //111
parseInt(0111); //八进制数 73
parseInt('');//NaN
parseInt('0X11'); //17
parseInt('1a') //1
parseInt('a1'); //NaN
parseInt(['10aa','aaa']);//10

parseInt([]);//NaN; parseInt(undefined);复制代码

parseFloat

规则和parseInt基本相同,接受一个Number类型或字符串,若是是字符串中,那么只有第一个小数点是有效的。

toString()

规则以下:

  • 若是是Number类型,输出数字字符串
  • 若是是 null 或者是 undefined,抛错
  • 若是是数组,那么将数组展开输出。空数组,返回''
  • 若是是对象,返回 [object Object]
  • 若是是Date, 返回日期的文字表示法
  • 若是是函数,输出对应的字符串(以下demo)
  • 若是是Symbol,输出Symbol字符串
let arry = [];
let obj = {a:1};
let sym = Symbol(100);
let date = new Date();
let fn = function() {console.log('稳住,咱们能赢!')}
let str = 'hello world';
console.log([].toString()); // ''
console.log([1, 2, 3, undefined, 5, 6].toString());//1,2,3,,5,6
console.log(arry.toString()); // 1,2,3
console.log(obj.toString()); // [object Object]
console.log(date.toString()); // Sun Apr 21 2019 16:11:39 GMT+0800 (CST)
console.log(fn.toString());// function () {console.log('稳住,咱们能赢!')}
console.log(str.toString());// 'hello world'
console.log(sym.toString());// Symbol(100)
console.log(undefined.toString());// 抛错
console.log(null.toString());// 抛错复制代码

String()

String() 的转换规则与 toString() 基本一致,最大的一点不一样在于 nullundefined,使用 String 进行转换,null 和 undefined对应的是字符串 'null''undefined'

Boolean

除了 undefined、 null、 false、 ''、 0(包括 +0,-0)、 NaN 转换出来是false,其它都是true.

隐式类型转换

&& 、|| 、 ! 、 if/while 的条件判断

须要将数据转换成 Boolean 类型,转换规则同 Boolean 强制类型转换

运算符: + - * /

+ 号操做符,不只能够用做数字相加,还能够用做字符串拼接。

仅当 + 号两边都是数字时,进行的是加法运算。若是两边都是字符串,直接拼接,无需进行隐式类型转换。

除了上面的状况外,若是操做数是对象、数值或者布尔值,则调用toString()方法取得字符串值(toString转换规则)。对于 undefined 和 null,分别调用String()显式转换为字符串,而后再进行拼接。

console.log({}+10); //[object Object]10
console.log([1, 2, 3, undefined, 5, 6] + 10);//1,2,3,,5,610复制代码

-*/ 操做符针对的是运算,若是操做值之一不是数值,则被隐式调用Number()函数进行转换。若是其中有一个转换除了为NaN,结果为NaN.

关系操做符: ==、>、< 、<=、>=

> , <<=>=

  1. 若是两个操做值都是数值,则进行数值比较
  2. 若是两个操做值都是字符串,则比较字符串对应的字符编码值
  3. 若是有一方是Symbol类型,抛出错误
  4. 除了上述状况以外,都进行Number()进行类型转换,而后再进行比较。

注: NaN是很是特殊的值,它不和任何类型的值相等,包括它本身,同时它与任何类型的值比较大小时都返回false。

console.log(10 > {});//返回false.
/**
 *{}.valueOf ---> {}
 *{}.toString() ---> '[object Object]' ---> NaN
 *NaN 和 任何类型比大小,都返回 false
 */复制代码

相等操做符:==

  1. 若是类型相同,无需进行类型转换。
  2. 若是其中一个操做值是 null 或者是 undefined,那么另外一个操做符必须为 null 或者 undefined 时,才返回 true,不然都返回 false.
  3. 若是其中一个是 Symbol 类型,那么返回 false.
  4. 两个操做值是否为 string 和 number,就会将字符串转换为 number
  5. 若是一个操做值是 boolean,那么转换成 number
  6. 若是一个操做值为 object 且另外一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断(调用object的valueOf/toString方法进行转换)

对象如何转换成原始数据类型

若是部署了 [Symbol.toPrimitive] 接口,那么调用此接口,若返回的不是基础数据类型,跑出错误。

若是没有部署 [Symbol.toPrimitive] 接口,那么先返回 valueOf() 的值,若返回的不是基础类型的值,再返回 toString() 的值,若返回的不是基础类型的值, 则抛出异常。

//先调用 valueOf, 后调用 toString
let obj = {
    [Symbol.toPrimitive]() {
        return 200;
    },
    valueOf() {
        return 300;
    },
    toString() {
        return 'Hello';
    }
}
//若是 valueOf 返回的不是基本数据类型,则会调用 toString, 
//若是 toString 返回的也不是基本数据类型,会抛出错误
console.log(obj + 200); //400复制代码


若是你有更好的答案或想法,欢迎在这题目对应的github下留言:JS 类型转换的规则是什么?

8.简述下对 webWorker 的理解?

HTML5则提出了 Web Worker 标准,表示js容许多线程,可是子线程彻底受主线程控制而且不能操做dom,只有主线程能够操做dom,因此js本质上依然是单线程语言。

web worker就是在js单线程执行的基础上开启一个子线程,进行程序处理,而不影响主线程的执行,当子线程执行完以后再回到主线程上,在这个过程当中不影响主线程的执行。子线程与主线程之间提供了数据交互的接口postMessage和onmessage,来进行数据发送和接收。

var worker = new Worker('./worker.js'); //建立一个子线程
worker.postMessage('Hello');
worker.onmessage = function (e) {
    console.log(e.data); //Hi
    worker.terminate(); //结束线程
};复制代码
//worker.js
onmessage = function (e) {
    console.log(e.data); //Hello
    postMessage("Hi"); //向主进程发送消息
};复制代码

仅是最简示例代码,项目中一般是将一些耗时较长的代码,放在子线程中运行。

若是你有更好的答案或想法,欢迎在这题目对应的github下留言:简述下对 webWorker 的理解

9.ES6模块和CommonJS模块的差别?

  1. ES6模块在编译时,就能肯定模块的依赖关系,以及输入和输出的变量。

    CommonJS 模块,运行时加载。

  2. ES6 模块自动采用严格模式,不管模块头部是否写了 "use strict"; (严格模式有哪些限制?[//连接])

  3. require 能够作动态加载,import 语句作不到,import 语句必须位于顶层做用域中。

  4. ES6 模块中顶层的 this 指向 undefined,ommonJS 模块的顶层 this 指向当前模块。

  5. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。如:

//name.js
var name = 'William';
setTimeout(() => name = 'Yvette', 200);
module.exports = {
    name
};
//index.js
const name = require('./name');
console.log(name); //William
setTimeout(() => console.log(name), 300); //William复制代码

对比 ES6 模块看一下:

ES6 模块的运行机制与 CommonJS 不同。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import ,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

//name.js
var name = 'William';
setTimeout(() => name = 'Yvette', 200);
export { name };
//index.js
import { name } from './name';
console.log(name); //William
setTimeout(() => console.log(name), 300); //Yvette复制代码

若是你有更好的答案或想法,欢迎在这题目对应的github下留言:ES6模块和CommonJS模块的差别?

10.浏览器事件代理机制的原理是什么?

在说浏览器事件代理机制原理以前,咱们首先了解一下事件流的概念,早期浏览器,IE采用的是事件捕获事件流,而Netscape采用的则是事件捕获。"DOM2级事件"把事件流分为三个阶段,捕获阶段、目标阶段、冒泡阶段。现代浏览器也都遵循此规范。


那么事件代理是什么呢?

事件代理又称为事件委托,在祖先级DOM元素绑定一个事件,当触发子孙级DOM元素的事件时,利用事件冒泡的原理来触发绑定在祖先级DOM的事件。由于事件会从目标元素一层层冒泡至document对象。

为何要事件代理?

  1. 添加到页面上的事件数量会影响页面的运行性能,若是添加的事件过多,会致使网页的性能降低。采用事件代理的方式,能够大大减小注册事件的个数。

  2. 事件代理的当时,某个子孙元素是动态增长的,不须要再次对其进行事件绑定。

  3. 不用担忧某个注册了事件的DOM元素被移除后,可能没法回收其事件处理程序,咱们只要把事件处理程序委托给更高层级的元素,就能够避免此问题。

如将页面中的全部click事件都代理到document上:

addEventListener 接受3个参数,分别是要处理的事件名、处理事件程序的函数和一个布尔值。布尔值默认为false。表示冒泡阶段调用事件处理程序,若设置为true,表示在捕获阶段调用事件处理程序。

document.addEventListener('click', function (e) {
    console.log(e.target);
    /**
    * 捕获阶段调用调用事件处理程序,eventPhase是 1; 
    * 处于目标,eventPhase是2 
    * 冒泡阶段调用事件处理程序,eventPhase是 1;
    */ 
    console.log(e.eventPhase);
    
});复制代码

若是你有更好的答案或想法,欢迎在这题目对应的github下留言:浏览器事件代理机制的原理是什么?


11.js如何自定义事件?

自定义 DOM 事件(不考虑IE9以前版本)

自定义事件有三种方法,一种是使用 new Event(), 另外一种是 createEvent('CustomEvent') , 另外一种是 new customEvent()

  1. 使用 new Event()

获取不到 event.detail

let btn = document.querySelector('#btn');
let ev = new Event('alert', {
    bubbles: true,    //事件是否冒泡;默认值false
    cancelable: true, //事件可否被取消;默认值false
    composed: false
});
btn.addEventListener('alert', function (event) {
    console.log(event.bubbles); //true
    console.log(event.cancelable); //true
    console.log(event.detail); //undefined
}, false);
btn.dispatchEvent(ev);复制代码
  1. 使用 createEvent('CustomEvent') (DOM3)

要建立自定义事件,能够调用 createEvent('CustomEvent'),返回的对象有 initCustomEvent 方法,接受如下四个参数:

  • type: 字符串,表示触发的事件类型,如此处的'alert'
  • bubbles: 布尔值: 表示事件是否冒泡
  • cancelable: 布尔值,表示事件是否能够取消
  • detail: 任意值,保存在 event 对象的 detail 属性中
let btn = document.querySelector('#btn');
let ev = btn.createEvent('CustomEvent');
ev.initCustomEvent('alert', true, true, 'button');
btn.addEventListener('alert', function (event) {
    console.log(event.bubbles); //true
    console.log(event.cancelable);//true
    console.log(event.detail); //button
}, false);
btn.dispatchEvent(ev);
复制代码复制代码
  1. 使用 new customEvent() (DOM4)

使用起来比 createEvent('CustomEvent') 更加方便

var btn = document.querySelector('#btn');
/*
 * 第一个参数是事件类型
 * 第二个参数是一个对象
 */
var ev = new CustomEvent('alert', {
    bubbles: 'true',
    cancelable: 'true',
    detail: 'button'
});
btn.addEventListener('alert', function (event) {
    console.log(event.bubbles); //true
    console.log(event.cancelable);//true
    console.log(event.detail); //button
}, false);
btn.dispatchEvent(ev);复制代码

自定义非 DOM 事件(观察者模式)

EventTarget类型有一个单独的属性handlers,用于存储事件处理程序(观察者)。

addHandler() 用于注册给定类型事件的事件处理程序;

fire() 用于触发一个事件;

removeHandler() 用于注销某个事件类型的事件处理程序。

function EventTarget(){
    this.handlers = {};
}

EventTarget.prototype = {
    constructor:EventTarget,
    addHandler:function(type,handler){
        if(typeof this.handlers[type] === "undefined"){
            this.handlers[type] = [];
        }
        this.handlers[type].push(handler);
    },
    fire:function(event){
        if(!event.target){
            event.target = this;
        }
        if(this.handlers[event.type] instanceof Array){
            const handlers = this.handlers[event.type];
            handlers.forEach((handler)=>{
                handler(event);
            });
        }
    },
    removeHandler:function(type,handler){
        if(this.handlers[type] instanceof Array){
            const handlers = this.handlers[type];
            for(var i = 0,len = handlers.length; i < len; i++){
                if(handlers[i] === handler){
                    break;
                }
            }
            handlers.splice(i,1);
        }
    }
}
//使用
function handleMessage(event){
    console.log(event.message);
}
//建立一个新对象
var target = new EventTarget();
//添加一个事件处理程序
target.addHandler("message", handleMessage);
//触发事件
target.fire({type:"message", message:"Hi"}); //Hi
//删除事件处理程序
target.removeHandler("message",handleMessage);
//再次触发事件,没有事件处理程序
target.fire({type:"message",message: "Hi"});复制代码

若是你有更好的答案或想法,欢迎在这题目对应的github下留言:js如何自定义事件?

12.跨域的方法有哪些?原理是什么?

知其然知其因此然,在说跨域方法以前,咱们先了解下什么叫跨域,浏览器有同源策略,只有当“协议”、“域名”、“端口号”都相同时,才能称之为是同源,其中有一个不一样,便是跨域。

那么同源策略的做用是什么呢?同源策略限制了从同一个源加载的文档或脚本如何与来自另外一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

那么咱们又为何须要跨域呢?一是前端和服务器分开部署,接口请求须要跨域,二是咱们可能会加载其它网站的页面做为iframe内嵌。

跨域的方法有哪些?

经常使用的跨域方法

  1. jsonp

尽管浏览器有同源策略,可是 <script> 标签的 src 属性不会被同源策略所约束,能够获取任意服务器上的脚本并执行。jsonp 经过插入script标签的方式来实现跨域,参数只能经过url传入,仅能支持get请求。

实现原理:

Step1: 建立 callback 方法

Step2: 插入 script 标签

Step3: 后台接受到请求,解析前端传过去的 callback 方法,返回该方法的调用,而且数据做为参数传入该方法

Step4: 前端执行服务端返回的方法调用

下面代码仅为说明 jsonp 原理,项目中请使用成熟的库。分别看一下前端和服务端的简单实现:

//前端代码
function jsonp({url, params, cb}) {
    return new Promise((resolve, reject) => {
        //建立script标签
        let script = document.createElement('script');
        //将回调函数挂在 window 上
        window[cb] = function(data) {
            resolve(data);
            //代码执行后,删除插入的script标签
            document.body.removeChild(script);
        }
        //回调函数加在请求地址上
        params = {...params, cb} //wb=b&cb=show
        let arrs = [];
        for(let key in params) {
            arrs.push(`${key}=${params[key]}`);
        }
        script.src = `${url}?${arrs.join('&')}`;
        document.body.appendChild(script);
    });
}
//使用
function sayHi(data) {
    console.log(data);
}
jsonp({
    url: 'http://localhost:3000/say',
    params: {
        //code
    },
    cb: 'sayHi'
}).then(data => {
    console.log(data);
});复制代码
//express启动一个后台服务
let express = require('express');
let app = express();

app.get('/say', (req, res) => {
    let {cb} = req.query; //获取传来的callback函数名,cb是key
    res.send(`${cb}('Hello!')`);
});
app.listen(3000);复制代码

从今天起,jsonp的原理就要了然于心啦~


  1. cors

jsonp 只能支持 get 请求,cors 能够支持多种请求。cors 并不须要前端作什么工做。

简单跨域请求:

只要服务器设置的Access-Control-Allow-Origin Header和请求来源匹配,浏览器就容许跨域

  1. 请求的方法是get,head或者post。
  2. Content-Type是application/x-www-form-urlencoded, multipart/form-data 或 text/plain中的一个值,或者不设置也能够,通常默认就是application/x-www-form-urlencoded。
  3. 请求中没有自定义的HTTP头部,如x-token。(应该是这几种头部 Accept,Accept-Language,Content-Language,Last-Event-ID,Content-Type)
//简单跨域请求
app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', 'XXXX');
});复制代码

带预检(Preflighted)的跨域请求

不满于简单跨域请求的,便是带预检的跨域请求。服务端须要设置 Access-Control-Allow-Origin (容许跨域资源请求的域) 、 Access-Control-Allow-Methods (容许的请求方法) 和 Access-Control-Allow-Headers (容许的请求头)

app.use((req, res, next) => {
    res.setHeader('Access-Control-Allow-Origin', 'XXX');
    res.setHeader('Access-Control-Allow-Headers', 'XXX'); //容许返回的头
    res.setHeader('Access-Control-Allow-Methods', 'XXX');//容许使用put方法请求接口
    res.setHeader('Access-Control-Max-Age', 6); //预检的存活时间
    if(req.method === "OPTIONS") {
        res.end(); //若是method是OPTIONS,不作处理
    }
});复制代码

更多CORS的知识能够访问: HTTP访问控制(CORS)

  1. nginx 反向代理

使用nginx反向代理实现跨域,只须要修改nginx的配置便可解决跨域问题。

A网站向B网站请求某个接口时,向B网站发送一个请求,nginx根据配置文件接收这个请求,代替A网站向B网站来请求。 nginx拿到这个资源后再返回给A网站,以此来解决了跨域问题。

例如nginx的端口号为 8090,须要请求的服务器端口号为 3000。(localhost:8090 请求 localhost:3000/say)

nginx配置以下:

server {
    listen       8090;

    server_name  localhost;

    location / {
        root   /Users/liuyan35/Test/Study/CORS/1-jsonp;
        index  index.html index.htm;
    }
    location /say {
        rewrite  ^/say/(.*)$ /$1 break;
        proxy_pass   http://localhost:3000;
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    }
    # others
}复制代码
  1. websocket

Websocket 是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通讯,同时也是跨域的一种解决方案。

Websocket 不受同源策略影响,只要服务器端支持,无需任何配置就支持跨域。

前端页面在 8080 的端口。

let socket = new WebSocket('ws://localhost:3000'); //协议是ws
socket.onopen = function() {
    socket.send('Hi,你好');
}
socket.onmessage = function(e) {
    console.log(e.data)
}复制代码

服务端 3000端口。能够看出websocket无需作跨域配置。

let WebSocket = require('ws');
let wss = new WebSocket.Server({port: 3000});
wss.on('connection', function(ws) {
    ws.on('message', function(data) {
        console.log(data); //接受到页面发来的消息'Hi,你好'
        ws.send('Hi'); //向页面发送消息
    });
});复制代码
  1. postMessage

postMessage 经过用做前端页面以前的跨域,如父页面与iframe页面的跨域。window.postMessage方法,容许跨窗口通讯,不论这两个窗口是否同源。

话说工做中两个页面以前须要通讯的状况并很少,我本人工做中,仅使用过两次,一次是H5页面中发送postMessage信息,ReactNative的webview中接收此此消息,并做出相应处理。另外一次是可轮播的页面,某个轮播页使用的是iframe页面,为了解决滑动的事件冲突,iframe页面中去监听手势,发送消息告诉父页面是否左滑和右滑。

子页面向父页面发消息

父页面

window.addEventListener('message', (e) => {
    this.props.movePage(e.data);
}, false);复制代码

子页面(iframe):

if(/*左滑*/) {
    window.parent && window.parent.postMessage(-1, '*')
}else if(/*右滑*/){
    window.parent && window.parent.postMessage(1, '*')
}复制代码

父页面向子页面发消息

父页面:

let iframe = document.querySelector('#iframe');
iframe.onload = function() {
    iframe.contentWindow.postMessage('hello', 'http://localhost:3002');
}复制代码

子页面:

window.addEventListener('message', function(e) {
    console.log(e.data);
    e.source.postMessage('Hi', e.origin); //回消息
});复制代码
  1. node 中间件

node 中间件的跨域原理和nginx代理跨域,同源策略是浏览器的限制,服务端没有同源策略。

node中间件实现跨域的原理以下:

1.接受客户端请求

2.将请求 转发给服务器。

3.拿到服务器 响应 数据。

4.将 响应 转发给客户端。

不经常使用跨域方法

如下三种跨域方式不多用,若有兴趣,可自行查阅相关资料。

  1. window.name + iframe

  2. location.hash + iframe

  3. document.domain (主域需相同)

若是你有更好的答案或想法,欢迎在这题目对应的github下留言:跨域的方法有哪些?原理是什么?

13.js异步加载的方式有哪些?

  1. <script> 的 defer 属性,HTML4 中新增

  2. <script> 的 async 属性,HTML5 中新增

<script>标签打开defer属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。

defer 和 async 的区别在于: defer要等到整个页面在内存中正常渲染结束,才会执行;

async一旦下载完,渲染引擎就会中断渲染,执行这个脚本之后,再继续渲染。defer是“渲染完再执行”,async是“下载完就执行”。

若是有多个 defer 脚本,会按照它们在页面出现的顺序加载。

多个async脚本是不能保证加载顺序的。

  1. 动态插入 script 脚本
function downloadJS() { 
    varelement = document.createElement("script"); 
    element.src = "XXX.js"; 
    document.body.appendChild(element); 
}
//什么时候的时候,调用上述方法 复制代码
  1. 有条件的动态建立脚本

如页面 onload 以后,

若是你有更好的答案或想法,欢迎在这题目对应的github下留言:js异步加载的方式有哪些?

14.下面代码a在什么状况中打印出1?

//?
if(a == 1 && a == 2 && a == 3) {
    console.log(1);
}
复制代码复制代码

1.在类型转换的时候,咱们知道了对象如何转换成原始数据类型。若是部署了 [Symbol.toPrimitive],那么返回的就是Symbol.toPrimitive的返回值。固然,咱们也能够把此函数部署在valueOf或者是toString接口上,效果相同。

//利用闭包延长做用域的特性
let a = {
    [Symbol.toPrimitive]: (function() {
            let i = 1;
            return function() {
                return i++;
            }
    })()
}复制代码

(1). 比较 a == 1 时,会调用 [Symbol.toPrimitive],此时 i 是 1,相等。 (2). 继续比较 a == 2,调用 [Symbol.toPrimitive],此时 i 是 2,相等。 (3). 继续比较 a == 3,调用 [Symbol.toPrimitive],此时 i 是 3,相等。

2.利用Object.definePropert在window/global上定义a属性,获取a属性时,会调用get.

let val = 1;
Object.defineProperty(window, 'a', {
  get: function() {
    return val++;
  }
});
复制代码复制代码

3.利用数组的特性。

var a = [1,2,3];
a.join = a.shift;复制代码

数组的 toString 方法返回一个字符串,该字符串由数组中的每一个元素的 toString() 返回值经调用 join() 方法链接(由逗号隔开)组成。

所以,咱们能够从新 join 方法。返回第一个元素,并将其删除。

若是你有更好的答案或想法,欢迎在这题目对应的github下留言:下面代码a在什么状况中打印出1?

15.下面这段代码的输出是什么?

function Foo() {
    getName = function() {console.log(1)};
    return this;
}
Foo.getName = function() {console.log(2)};
Foo.prototype.getName = function() {console.log(3)};
var getName = function() {console.log(4)};
function getName() {console.log(5)};

Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();复制代码

**说明:**一道经典的面试题,仅是为了帮助你们回顾一下知识点,加深理解,真实工做中,是不可能这样写代码的,不然,确定会被打死的。

1.首先预编译阶段,变量声明与函数声明提高至其对应做用域的最顶端。

所以上面的代码编译后以下(函数声明的优先级先于变量声明):

function Foo() {
    getName = function() {console.log(1)};
    return this;
}
var getName;
function getName() {console.log(5)};
Foo.getName = function() {console.log(2)};
Foo.prototype.getName = function() {console.log(3)};
getName = function() {console.log(4)};复制代码

2.Foo.getName();直接调用Foo上getName方法,输出2

3.getName();输出4,getName被从新赋值了

4.Foo().getName();执行Foo(),window的getName被从新赋值,返回this;浏览器环境中,非严格模式,this 指向 window,this.getName();输出为1.

若是是严格模式,this 指向 undefined,此处会抛出错误。

若是是node环境中,this 指向 global,node的全局变量并不挂在global上,由于global.getName对应的是undefined,不是一个function,会抛出错误。

5.getName();已经抛错的天然走不动这一步了;继续浏览器非严格模式;window.getName被从新赋过值,此时再调用,输出的是1

6.new Foo.getName();考察运算符优先级的知识,new 无参数列表,对应的优先级是18;成员访问操做符 . , 对应的优先级是 19。所以至关因而 new (Foo.getName)();new操做符会执行构造函数中的方法,所以此处输出为 2.

7.new Foo().getName();new 带参数列表,对应的优先级是19,和成员访问操做符.优先级相同。同级运算符,按照从左到右的顺序依次计算。new Foo()先初始化 Foo 的实例化对象,实例上没有getName方法,所以须要原型上去找,即找到了 Foo.prototype.getName,输出3

8.new new Foo().getName(); new 带参数列表,优先级19,所以至关因而 new (new Foo()).getName();先初始化 Foo 的实例化对象,而后将其原型上的 getName 函数做为构造函数再次 new ,输出3

所以最终结果以下:

Foo.getName(); //2
getName();//4
Foo().getName();//1
getName();//1
new Foo.getName();//2
new Foo().getName();//3
new new Foo().getName();//3复制代码

若是你有更好的答案或想法,欢迎在这题目对应的github下留言:下面这段代码的输出是什么?

16.实现双向绑定 Proxy 与 Object.defineProperty 相比优劣如何?

  1. Object.definedProperty 的做用是劫持一个对象的属性,劫持属性的getter和setter方法,在对象的属性发生变化时进行特定的操做。而 Proxy 劫持的是整个对象。

  2. Proxy 会返回一个代理对象,咱们只须要操做新对象便可,而 Object.defineProperty 只能遍历对象属性直接修改。

  3. Object.definedProperty 不支持数组,更准确的说是不支持数组的各类API,由于若是仅仅考虑arry[i] = value 这种状况,是能够劫持的,可是这种劫持意义不大。而 Proxy 能够支持数组的各类API。

  4. 尽管 Object.defineProperty 有诸多缺陷,可是其兼容性要好于 Proxy.

PS: Vue2.x 使用 Object.defineProperty 实现数据双向绑定,V3.0 则使用了 Proxy.

//拦截器
let obj = {};
let temp = 'Yvette';
Object.defineProperty(obj, 'name', {
    get() {
        console.log("读取成功");
        return temp
    },
    set(value) {
        console.log("设置成功");
        temp = value;
    }
});

obj.name = 'Chris';
console.log(obj.name);复制代码

PS: Object.defineProperty 定义出来的属性,默认是不可枚举,不可更改,不可配置【没法delete】

咱们能够看到 Proxy 会劫持整个对象,读取对象中的属性或者是修改属性值,那么就会被劫持。可是有点须要注意,复杂数据类型,监控的是引用地址,而不是值,若是引用地址没有改变,那么不会触发set。

let obj = {name: 'Yvette', hobbits: ['travel', 'reading'], info: {
    age: 20,
    job: 'engineer'
}};
let p = new Proxy(obj, {
    get(target, key) { //第三个参数是 proxy, 通常不使用
        console.log('读取成功');
        return Reflect.get(target, key);
    },
    set(target, key, value) {
        if(key === 'length') return true; //若是是数组长度的变化,返回。
        console.log('设置成功');
        return Reflect.set([target, key, value]);
    }
});
p.name = 20; //设置成功
p.age = 20; //设置成功; 不须要事先定义此属性
p.hobbits.push('photography'); //读取成功;注意不会触发设置成功
p.info.age = 18; //读取成功;不会触发设置成功复制代码

最后,咱们再看下对于数组的劫持,Object.definedProperty 和 Proxy 的差异

Object.definedProperty 能够将数组的索引做为属性进行劫持,可是仅支持直接对 arry[i] 进行操做,不支持数组的API,很是鸡肋。

let arry = []
Object.defineProperty(arry, '0', {
    get() {
        console.log("读取成功");
        return temp
    },
    set(value) {
        console.log("设置成功");
        temp = value;
    }
});

arry[0] = 10; //触发设置成功
arry.push(10); //不能被劫持复制代码

Proxy 能够监听到数组的变化,支持各类API。注意数组的变化触发get和set可能不止一次,若有须要,自行根据key值决定是否要进行处理。

let hobbits = ['travel', 'reading'];
let p = new Proxy(hobbits, {
    get(target, key) {
        // if(key === 'length') return true; //若是是数组长度的变化,返回。
        console.log('读取成功');
        return Reflect.get(target, key);
    },
    set(target, key, value) {
        // if(key === 'length') return true; //若是是数组长度的变化,返回。
        console.log('设置成功');
        return Reflect.set([target, key, value]);
    }
});
p.splice(0,1) //触发get和set,能够被劫持
p.push('photography');//触发get和set
p.slice(1); //触发get;由于 slice 是不会修改原数组的复制代码

若是你有更好的答案或想法,欢迎在这题目对应的github下留言:实现双向绑定 Proxy 与 Object.defineProperty 相比优劣如何?

17.Object.is() 与比较操做符 ===、== 有什么区别?

如下状况,Object.is认为是相等

两个值都是 undefined
两个值都是 null
两个值都是 true 或者都是 false
两个值是由相同个数的字符按照相同的顺序组成的字符串
两个值指向同一个对象
两个值都是数字而且
都是正零 +0
都是负零 -0
都是 NaN
都是除零和 NaN 外的其它同一个数字复制代码

Object.is() 相似于 ===,可是有一些细微差异,以下:

  1. NaN 和 NaN 相等
  2. -0 和 +0 不相等
console.log(Object.is(NaN, NaN));//true
console.log(NaN === NaN);//false
console.log(Object.is(-0, +0)); //false
console.log(-0 === +0); //true复制代码

Object.is 和 ==差得远了, == 在类型不一样时,须要进行类型转换,前文已经详细说明。

本文转载自 寒冬求职季之你必需要懂的原生JS

相关文章
相关标签/搜索