前端八股文之javascript

3,970 阅读36分钟

题目加答案,每日更新5~10题

目录

待输出

  • 使用 Symbol 函数都有哪些要注意的点?
  • Symbol 值的强制类型转换?
  • Array 是 Object 类型么?
  • 如何获取 html 元素实际的样式值?
  • 如果我不想让别人对 obj 对象添加或者删除元素,可以怎么做呢?
  • 用 for 模拟实现 forEach
  • 在 map 中和 for 中调用异步函数的区别?
  • 除了 jsonp、postmessage 后端控制,怎么实现跨页面通讯?
  • 说一下对事件流的理解
  • JS 是多线程么?绑定一个事件的回调函数是宏任务还是微任务?
  • 说一下事件循环机制(node 浏览器)
  • JS为什么要区分微任务和宏任务?
  • 在 ES6 中有哪些解决异步的方法?
  • 异步编程的实现方式是什么?
  • js异步解决方案有哪几种
  • nextTick 是在本次循环执行,还是在下次循环执行呢?setTimeout(()=>{},1000)是怎样执行的呢?
  • Generator yield 的作用
  • Promise.allSettled 了解么?试着手写下 Promise.allSettled.
  • Promise 构造函数是同步还是异步执行,then呢?
  • promise有没有解决异步的问题?
  • Promise.resolve(obj),obj有几种形式
  • js继承的几种实现方式?
  • class 的继承和 prototype 继承 是完全一样的么?
  • ES6 类继承中 super 的作用
  • 使用原型最大的好处
  • prototype 和 proto 区别
  • 描述下JS中Prototype的概念?
  • 说明下JS继承的原理?
  • Token 一般是存放在哪里?
  • Token 放在 cookie 和放在 localStorage、sessionStorage 中有什么不同?
  • 箭头函数和普通函数的区别?
  • JavaScript 中如何模拟实现方法的重载?
  • 说一下递归和迭代的区别是什么,各有什么优缺点?
  • 说一下 JavaScript 的宿主对象和原生对象的区别?
  • 说说ES6对Object类型做了哪些优化更新?
  • 介绍class和ES5的类以及区别
  • require模式引入的查找方式是什么?
  • 请说出目前主流的js模块化实现的技术有哪些?他们的区别在哪儿?
  • 使用 Object.defineProperty() 来进行数据劫持有什么缺点?
  • 说一下你所了解的javascript的作用域链?
  • 什么是DOM和BOM?
  • 如何判断一个对象是否属于某个类?
  • for..in和Object.keys的区别
  • 三种事件模型是什么?
  • Object.is()与原来的比较操作符 "===" 、"==" 的区别?
  • 类数组转化为数组
  • 回调函数和任务队列的区别
  • 怎么处理项目中的异常捕获行为?
  • 词法作用域和this的区别
  • 说一下变量的作用域链
  • 请用JS代码实现事件代理
  • 介绍js全部数据类型,基本数据类型和引用数据类型的区别

已完结

  • 使用 Symbol 函数都有哪些要注意的点?
  • Symbol 值的强制类型转换?
  • 说一下 JavaScript 严格模式下有哪些不同?
  • javascript 代码中的"use strict"是什么意思?为什么使用它?
  • 用原生js实现自定义事件
  • 什么是功能检测、功能推断、UA 字符串?
  • 说一下对 BigInt 的理解,在什么场景下会使用?
  • 说一下 let、const 的实现,使用 ES5 模拟实现
  • Reflect.ownKeys 与 Object.keys 的区别
  • Reflect 对象创建目的是什么?
  • addEventListener在removeListener之后不会造成内存泄露?
  • 内部属性[[Class]]是什么?
  • 什么是 Polyfill ?
  • 使用TS的优势有哪些?
  • TypeScript里面有哪些JavaScript没有的类型?
  • 谈谈你对ajax的理解?
  • 异步请求,低版本 fetch 如何低版本适配?
  • 如何创建一个 ajax?
  • 说一下ajax/axios/fetch三者的区别
  • 定时器为什么是不精确的?
  • 为什么使用 setTimeout 实现 setInterval?怎么模拟?
  • setInterval 需要注意的点
  • setTimeout 有什么缺点,和 requestAnimationFrame 有什么区别?
  • 说一下 setTimeout 和 setInterval 在内存方面的区别?
  • setTimeout/setInterval 实现倒计时如何解决时间偏差的问题?
  • 哪些操作会造成内存泄漏?
  • 说一说对JSON的理解?
  • 深拷贝如何解决循环引用?
  • 手写实现 js 中的深拷贝
  • 实现柯里化函数
  • 手动实现jsonp
  • 防抖节流原理、区别以及应用,请用js实现
  • 说一下栈和堆的区别,垃圾回收时栈和堆的区别?
  • 说一下import的原理,和require的不同之处在哪儿?

使用 Symbol 函数都有哪些要注意的点?

Symbol

ES6 中引入了—种新的基础数据类型: Symbol,这是一种新的基础数据类型(primitive type)

它的功能类似于一种标识唯一性的 ID。通常情况下,我们可以通过调用 Symbol()函数来创建一个 Symbol 实例:

let s1 = Symbol();

或者,你也可以在调用 Symbol() 函数时传入一个可选的字符串参数,相当于给你创建的 Symbol 实例一个描述信息:

let s2 = Symbol("another symbol");

如果用当下比较流行的 TypeScript 的方式来描述这个 Symbol() 函数的话,可以表示成:

/**
 *  @param {any} description 描述信息。可以是任何可以被转型成字符串的值,如:字符串、数字、对象、数组等
 */
function Symbol(description: any): symbol;

由于 Symbo1 是一种基础数据类型,所以当我们使用 typeof 去检查它的类型的时候,它会返回一个属于自己的类型 symbol,而不是什么 string、object 之类的:

typeof s1; // 'symbol'

另外,我们需要重点记住的一点是: 每个 Symbol 实例都是唯一的。因此,当你比较两个 Symbol 实例的时候,将总会返回 false:

let s1 = Symbol();
let s2 = Symbol("another symbol");
let s3 = Symbol("another symbol");

s1 === s2; //false
s2 === s3; //false

应用场景

  • 使用 Symbol 来作为对象属性名(key)

在这之前,我们通常定义或访问对象的属性时都是使用字符串,比如下面的代码:

let obj = {
  num: 123,
  txt: "world",
};

obj["num"]; // 123
obj["txt"]; // 'world'

而现在 symbol 可同样用于对象属性的定义和访问:

const PROP_NAME = Symbol();
const PROP_AGE = Symbol();

let obj = {
  [PROP_NAME]: "sunny",
};
obj[PROP_AGE] = 18;

obj[PROP_NAME]; // sunny
obj[PROP_AGE]; // 18

随之而来的是另一个非常值得注意的问题: 就是当使用了 Symbol 作为对象的属性 key 后,在对该对象进行 key 的枚举时,会有什么不同?在实际应用中,我们经常会需要使用 object.keys ()或者 for. ..in 来枚举对象的属性名,那在这方面,Symbo1 类型的 key 表现的会有什么不同之处呢?来看以下示例代码:

let obj = {
  [Symbol("name")]: "sunny",
  age: 18,
  title: "symbol",
};
Object.keys(obj); // ["age","title"]
for (let p in obj) {
  console.log(p); // 分别输出 “age” 和 “title”
}
Object.getOwnPropertyNames(obj); // ["age","title"]

由上代码可知,Symbol 类型的 key 是不能通过 object.keys() 或者 for…in 来枚举的,它未被包含在对象自身的属性名集合(property names)之中。所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用 Symbol 来定义。

也正因为这样一个特性,当使用 JSON.stringify() 将对象转换成 JSON 字符串的时候,Symbol 属性也会被排除在输出内容之外:

JSON.stringify(obj); // {"age":18,"title":"symbol"}

我们可以利用这一特点来更好的设计我们的数据对象,让“对内操作"和“对外选择性输出”变得更加优雅。然而,这样的话,我们就没办法获取以 Symbol 方式定义的对象属性了么?不一定。还是会有一些专门针对 Symbo1 的 API,比如:

// 使用Object的API
Object.getOwnPropertySymbol(obj); // [Symbol(name)]

// 使用新增的的反射API
Reflect.ownKeys(obj); // [Symbol(name),"age","title"]

使用 Symbol 代替常量

先来看看下面的代码:

const TYPE_AUDIO = "AUDIO";
const TYPE_VIDEO = "VIDEO";
const TYPE_IMAGE = "IMAGE";

function handleFileResource(resource) {
  switch (resource.type) {
    case TYPE_AUDIO:
      playAudio(resource);
      break;
    case TYPE_VIDEO:
      playVideo(resource);
      break;
    case TYPE_IMAGE:
      previewImage(resource);
      break;
    default:
      throw new Error("Unknown type of resource");
  }
}

如上面的代码中那样,我们经常定义一组常量来代表一种业务逻辑下的几个不同类型,我们通常希望这几个常量之间是唯一的关系,为了保证这一点,我们需要为常量赋—个唯一的值(比如这里的'AUDIO、'VIDEO'、'IMAGE'),常量少的时候还算好,但是常量一多,你可能还得花点脑子好好为他们取个好点的名字。

现在有了 Symbo1,我们大可不必这么麻烦了:

const TYPE_AUDIO = Symbol();
const TYPE_VIDEO = Symbol();
const TYPE_IMAGE = Symbol();

这样定义,就能保证三个常量的值是唯一的了!

使用 Symbol 定义类的私有属性或方法

我们知道在 JavaScript 中,是没有如 Java 等面向对象语言的访问控制关键字 private 的,类上所有定义的属性或方法都是可公开访问的。因此这对我们进行 API 的设计时造成了一些困扰。而有了 Symbol 以及模块化机制,类的私有属性和方法才变成可能。例如:

//a.js
const PASSWORD = Symbol();

class Login {
  constructor(userName, password) {
    this.userName = userName;
    this[PASSWORD] = password;
  }
  checkPassword(pwd) {
    return this[PASSWORD] === pwd;
  }
}
export default Login;
// b.js
import Login from "./a";

const login = new Login("admin", "123456");

login.checkPassword("admin"); // true

login.PASSWORD; // oh!no!
login[PASSWORD]; // oh!no!
login["PASSWORD"]; // oh!no!

由于 Symbol 常量 PASSWORD 被定义在 a.js 所在的模块中,外面的模块获取不到这个 Symbol,也不可能再创建一个一模一样的 Symbol 出来(因为 Symbol 是唯一的),因此这个 PASSWORD 的 Symbol 只能被限制在 a.js 内部使用,所以使用它来定义的类属性是没有办法被模块外访问到的,达到了一个私有化的效果。

应用和需要注意的点

注册和获取全局 Symbol 通常情况下,我们在一个浏览器窗口中(window),使用 Symbol()函数定义 Symbol 实力就足够啦,但是,如果你的应用涉及到多个 window(最典型的就是页面中使用了iframe),并需要这些 window 中使用的某些 Symbol 是同一个,那就不能使用 Symbol ()函数了,因为用它在不同 window 中创建的 Symbol 实例总是唯一的,而我们需要的是在所有这些 window 环境下保持一个共享的 Symbol。这种情况下,我们就需要使用另一个 API 来创建或获取 Symbol,那就是 Symbol.for() ,它可以注册或获取一个 window 间全局的 Symbol 实例:

let gs1 = Symbol.for("global_symbol_1"); // 注册一个全局Symbol
let gs2 = Symbol.for("global_symbol_2"); // 获取全局Symbol

gs1 === gs2; // true

这样一个 Symbol 不光在但在 window 中是唯一的,在多个相关的 window 间也是唯一的了

注意的点:

  • Symbol 函数前不能使用 new 命令,否则会报错。
  • Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
  • Symbo1 作为属性名,该属性不会出现在 for…in 、 for…of 循环中,也不会被 object.keys()、object.getOwnPropertyNames() 、JSON.stringify ()返回。
  • object.getOwnPropertySymbols 方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
  • Symbol.for 接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
  • Symbol.keyFor 方法返回一个已登记的 Symbol 类型值的 key。

Symbol 值的强制类型转换?

ES6 允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误。

Symbol 值不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式结果都是 true)

类型转换是JS语言重要的一部分,能够非常灵活地将一种数据类型转换为另一种。然而Symbol类型在进行转换时非常不灵活,因为其他类型缺乏与 Symbol 值的合理等价,尤其是 Symbol 无法被转换为字符串值或数值'。

String()转换

虽然说上面说不可以转换,其实只能转换成 Symbol(uid) 这种形式,想取到'描述内容'需要手动截取'描述内容'

let uid = Symbol.for("uid"), // 创建全局私有属性
    test = Symbol('呵呵哒'), // 创建局部私有属性
    desc = String(uid), // 转换成字符串
    testString = String(test) // 转换成字符串

console.log(desc)    // "Symbol(uid)"
console.log(testString)    // "Symbol(呵呵哒)"

强制类型转换

uid 与空字符串相连接,会首先要求把 uid 转换为一个字符串,而这会引发错误,从而阻止了转换行为。

let uid = Symbol.for("uid"),
    desc = uid + ""; // 引发错误!

相似地,你不能将 Symbol 转换为数值,对 Symbol 使用所有数学运算符都会引发错误,例如:

let uid = Symbol.for("uid"),
    sum = uid / 1; // 引发错误!

此例试图把 Symbol 除以 1 ,同样引发了错误。无论对 Symbol 使用哪种数学运算符都会导致错误,但使用

逻辑运算符则不会,因为 Symbol逻辑运算中会被认为等价于 true(就像 JS 中其他的非空值那样)。

说一下 JavaScript 严格模式下有哪些不同?

  1. 不允许使用var 关键字定义全局变量

  2. 不允许对变量使用 delete 操作符,抛 ReferenceError

  3. 不可对对象的只读属性赋值,不可对对象的不可配置属性使用 delete 操作符,不可为不可拓展的对象添加属性,均抛 TypeError

  4. 对象属性名必须唯一

  5. 函数中不可有重名参数

  6. 在函数内部对修改参数不会反映到 arguments 中

  7. 淘汰 arguments.callee 和 arguments.caller

  8. 不可在 if 内部声明函数

  9. 抛弃 with 语句

javascript 代码中的"use strict"是什么意思?为什么使用它?

use strict 是一种 ECMAscript5 添加的(严格)运行模式,这种模式使得 Javascript 在更严格的条件下运行。

设立"严格模式"的目的,主要有以下几个:

  • 消除 Javascript 语法的一些不合理、不严谨之处,减少一些怪异行为
  • 消除代码运行的一些不安全之处,保证代码运行的安全
  • 提高编译器效率,增加运行速度
  • 为未来新版本的 Javascript 做好铺垫

use strict 指的是严格运行模式,在这种模式对 js 的使用添加了一些限制。比如说禁止 this 指向全局对象,还有禁止使用 with 语句等。设立严格模式的目的,主要是为了消除代码使用中的一些不安全的使用方式,也是为了消除 js 语法本身的一些不合理的地方,以此来减少一些运行时的怪异的行为。同时使用严格运行模式也能够提高编译的效率,从而提高代码的运行速度。我认为严格模式代表了 js 一种更合理、更安全、更严谨的发展方向。

用原生js实现自定义事件

JS自定义事件

Javascript自定义事件类似设计的观察者模式,通过状态的变更来监听行为,主要功能解耦、易于扩展。多用于组件、模块间的交互

var EventTarget = function() {
    this._listener = {};
};

EventTarget.prototype = {
    constructor: this,
    addEvent: function(type, fn) {
        if (typeof type === "string" && typeof fn === "function") {
            if (typeof this._listener[type] === "undefined") {
                this._listener[type] = [fn];
            } else {
                this._listener[type].push(fn);
            }
        }
        return this;
    },
    addEvents: function(obj) {
        obj = typeof obj === "object" ? obj : {};
        var type;
        for (type in obj) {
            if ( type && typeof obj[type] === "function") {
                this.addEvent(type, obj[type]);
            }
        }
        return this;
    },
    fireEvent: function(type) {
        if (type && this._listener[type]) {
            //event参数设置
            var events = {
                type: type,
                target: this
            };

            for (var length = this._listener[type].length, start=0; start<length; start+=1) {
                //改变this指向
                this._listener[type][start].call(this, events);
            }
        }
        return this;
    },
    fireEvents: function(array) {
        if (array instanceof Array) {
            for (var i=0, length = array.length; i<length; i+=1) {
                this.fireEvent(array[i]);
            }
        }
        return this;
    },
    removeEvent: function(type, key) {
        var listeners = this._listener[type];
        if (listeners instanceof Array) {
            if (typeof key === "function") {
                for (var i=0, length=listeners.length; i<length; i+=1){
                    if (listeners[i] === key){
                        listeners.splice(i, 1);
                        break;
                    }
                }
            } else if (key instanceof Array) {
                for (var lis=0, lenkey = key.length; lis<lenkey; lis+=1) {
                    this.removeEvent(type, key[lenkey]);
                }
            } else {
                delete this._listener[type];
            }
        }
        return this;
    },
    removeEvents: function(params) {
        if (params instanceof Array) {
            for (var i=0, length = params.length; i<length; i+=1) {
                this.removeEvent(params[i]);
            }
        } else if (typeof params === "object") {
            for (var type in params) {
                this.removeEvent(type, params[type]);
            }
        }
        return this;
    }
};

什么是功能检测、功能推断、UA 字符串?

功能检测(feature detection)

功能检测包括确定浏览器是否支持某段代码,以及是否运行不同的代码(取决于它是否执行),以便浏览器使用能正常运行代码的功能,而不会在某些浏览器中出现崩溃和错误

if ("geolocation" in navigator) {
  // 可以使用 navigator.geolocation
} else {
  // 处理navigator.geolocation 功能缺失
}

modernizr库是个不错的功能检测工具。

功能推断(feature inference)

功能推断和功能检测一样,会对功能可用性进行检查,但是在判断过后,还会使用其他功能,因为它假设其他功能也可用

if (document.getElementByTagName) {
  element = document.getElementById(id);
}

这种方式不推荐,功能检测更能确保万无一失

UA字符串

这是一个浏览器报告的字符串,它允许网络协议对等方(network protocol peers)识别请求用户代理的类型、操作系统、应用供应商和应用版本,它可以通过navigator.userAgent访问。然而,这个字符串很难解析并且很可能存在欺骗性。例如,Chrome会同时作为 Chrome和Safari进行报告。因此,要检测Safari,除了检查Safari字符串,还要检查是否存在Chrome字符串

说一下对 BigInt 的理解,在什么场景下会使用?

BigInt

javascript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两个限制

  • 导致JS中的Number无法精确表示非常大的整数,它会将非常大的整数四舍五入,确切地说,JS中的Number类型只能安全地表示-9007199254740991(-(2^53-1))和9007199254740991((2^53-1)),任何超出此范围的整数值都可能失去精度。

  • 大于或等于 2 的 1024 次方的数值,javascript 无法表示,会返回 Infinity(无穷)

使用场景

在对大整数执行数学运算时,以任意精度表示整数的能力尤为重要,比如应用于科学和金融方面的计算.

使用 BigInt

为了解决这个问题,javascript 新增了基本数据类型 BigInt,目的就是比 Number 数据类型支持的范围更大的整数值

使用 Javascript 提供给的 BigInt 对象,可以用作构造函数生成 BigInt 类型的数值

转换规则基本与 Number()一致,将其他类型的值转化位 BigInt

BigInt(123); // 123n
BigInt("123"); // 123n
BigInt(false); // 0n
BigInt(true); // 1n

BigInt()构造函数必须有参数,而且参数必须可以正常转为数值,下面的用法都会报错.

new BigInt(); // TypeError
BigInt(undefined); //TypeError
BigInt(null); // TypeError
BigInt("123n"); // SyntaxError
BigInt("abc"); // SyntaxError

或直接在整数末尾追加 n 即可,比如:

console.log(9999999999999999n); // 9999999999999999n
console.log(9999999999999999); // 10000000000000000

在类型判断上,不能使用严格运算符去比较 BigInt 与 Number 类型,它们的类型不同因此为 false, 而相等运算符则会进行隐式类型转换, 因此为 true

console.log(100n === 100); // false
console.log(100n == 100); // true

BigInt 无法使用+ 一元运算符,也无法和 Number 一同计算

+10n; // Uncaught TypeError: Cannot convert a BigInt value to a number
20n / 10; // Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions

当使用 BigInt 进行计算时,结果同样会返回 BigInt 值.并且除法(/)运算符的结果会自动向下舍入到最接近的这个数

25n / 10n; // 10n

总结

BigInt 是一种新的基本类型,用于当整数值大于 Number 数据类型的范围时.使用 BigInt 避免整数溢出,保证计算安全.使用过程中要避免 BigInt 与 Number 和+一元运算符同时使用

说一下 let、const 的实现,使用 ES5 模拟实现

  • let使用立即执行函数实现
  • const用Object.defineProperty实现

ES6 let、const 块作用域实现原理

作用域是指在运行时代码中某些特定部分中变量、函数和对象的可访问性。可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去;也就是作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突

编译阶段

    1. 通过 var 声明的变量,会提升到函数变量环境中
    1. let、const 声明的变量会被推入词法环境中
    1. 在代码块内,通过 let、const 声明的变量,既不会推入变量环境,也不会推入词法环境。

执行阶段

    1. 代码执行到代码块,在代码块内,通过 let、const 声明的变量会被推入到词法环境,因此在代码块内这些变量是可访问的
    1. 当代码块执行结束时,在代码块内,通过 let、const 声明的变量会从词法环境中推出,因此在代码块外访问不到这些变量

let、const 的实现

let、const 块级声明用于声明在指定块的作用域之外无法访问的变量。let 用于声明变量,const 用于声明不可改变的常量

看下 babel 的转化

// 源代码
var a = 2;
{
  let a = 3;
  console.log(a); // 3
}
console.log(a); // 2

// babel转换后的代码
var a = 2;
{
  var _a = 3;
  console.log(_a); // 3
}
console.log(a); // 2
// 可以看到使用的是babel的转换是使用var

通过立即执行函数实现 let

var a = 2;
(function () {
  var a = 3;
  console.log(a); // 3
})();

console.log(a); // 2

通过 object.defineProperty(obj,prop,desc)实现 const

由于 ES5 环境没有 block 的概念,所以是无法百分之百实现 const,只要挂载到某个属性下,要么是全局的 window 要么就是自定义一个 object 来当容器

function _const(data, value) {
  Object.defineProperty(window, data, {
    enumerable: false,
    configurable: false,
    get: function () {
      return value;
    },
    set: function (data) {
      throw new TypeError("Assignment to constant variable.");
    },
  });
}
_const("a", [1, 2]);
a = [3, 4]; // 报错
a.push(3);

注意事项

const 实际上保证的并不是变量的值不许改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数值,值就保存在变量指向的内存地址,因此等同于常量。但是对于复杂类型的数值(数组,对象等),变量指向的内存地址,保存的是一个指向实际数值的指针,const只能保证这个指针式固定的(即是总指向另一个固定的地址),至于它指向的数据结构是不是可变的就说不清楚了。因此,将一个对象声明为常量的时候就必须小心使用了。

Reflect.ownKeys 与 Object.keys 的区别

  • 两者得到的都是对象属性的集合,以数组形式返回
  • Object.keys()得出的对象的可枚举属性,并且不包括原型上的属性和 Symbol 的属性
  • Reflect.ownKeys()得出的对象自己的所有属性,包括不可枚举和 Symbol 的属性,但是拿不到原型上的属性
Object.prototype.pr = "我是原型属性";
let s = Symbol();
let obj = {
  [s]: "this is Symbol",
  a: "a",
};
Object.defineProperty(obj, "name", {
  value: "sunny",
  configurable: true,
  enumerable: false,
  writable: true,
});

console.log("Object.keys", Object.keys(obj)); // ["a"]
console.log("Reflect.ownKeys(obj)", Reflect.ownKeys(obj)); //["a", "sunny", Symbol()]

Reflect 对象创建目的是什么?

Reflect对象其实就是为了取代Object对象。取代原因有一下几点:

  • 将 Object 对 象 的 一 些 明 显 属 于 语 言 内 部 的 方 法 ( 比 如Object.defineProperty)放到 Reflect 对象上。
  • 修改某些 Object 方法的返回结果,让其变得更合理。
  • 让 Object 操作都变成函数行为。
  • Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。

总而言之,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。

addEventListener在removeListener之后不会造成内存泄露?

  • 不会
造成内存泄漏的原因有:

不恰当的缓存
队列太长消费不及时
作用域未释放

尤其是处理函数中可能引用着父级作用域,导致作用域中的数据不能被回收。所以在恰当的时候,移除监听事件是很有必要的

如今浏览器添加原生事件

var button=document.getElementById('button');
function onClik(event){
    button.innerHTML='text';
}
button.addEventListener('click',onClick)

给元素button添加了一个事件处理器onClick,而处理器里面使用了button的引用。而老版本的IE是无法检测DOM节点和JS代码之间的循环引用,因此会导致内存泄漏

如今,现代的浏览器(包括IE和Microsoft Edge)使用了更先进的垃圾回收算法,已经可以正确检测和处理循环引用了。换言之,回收节点内存时,不必非要调用removeEventListener啦!

内部属性[[Class]]是什么?

所有typeof返回值为"object"的对象(比如数组)都包含一个内部属性[[Class]],我们可以把它看作一个内部的分类,而非传统的面向对象意义上的类。这个属性无法直接访问,一般通过Object.prototype.toString()来查看。

Object.prototype.toString.call([1,2,3])
//"[object Array]"
Object.prototype.toString.call(function(){})
//"[object Function]"

什么是 Polyfill ?

Polyfill 指的是用于实现浏览器并不支持的原生 API 的代码。

比如说 querySelectorAll 是很多现代浏览器都支持的原生 Web API,但是有些古老的浏览器并不支持,那么假设有人写了一段代码来实现这个功能使这些浏览器也支持了这个功能,那么这就可以成为一个 Polyfill。

一个 shim 是一个库,有自己的 API,而不是单纯实现原生不支持的 API。

使用TS的优势有哪些?

优势

  • 类型检测:在TS中为变量制定具体类型时,IDE会做出类型检测,这个特性减少在开发阶段犯错几率
  • 语法提示:在IDE里编写TS代码时,IDE会根据当前的上下文把类、变量、方法和关键字都给你提示出来,提高开发效率
  • 便于重构:修改变量或者方法的名字很边界,当你做出修改的时候,IDE会帮你自动引用这个变量或者调用这个方法的代码自动帮你修改
  • 活跃社区:TS拥抱ES6的规范,也支持部分起草规范,大部分的第三方库提供TS类型定义的文件

TypeScript的最大特点是静态类型,不同于JavaScript的动态类型,静态类型有以下优势:

  • 第一,静态类型检查可以做到early fail,编写的代码即使没有被执行到,但是发生类型不匹配的时候,语言在编译阶段(解释执行也一样),可以在运行前发现
  • 第二,静态类型对阅读代码是友好的,针对大型应用,方法很多,调用关系复杂,不可能每个函数都有人编写细致的文档,所以静态类型就是非常重要的提示和约束。此外TS还实现了类、接口、枚举、泛型,方法重载等语法糖,方便前端开发

TypeScript里面有哪些JavaScript没有的类型?

  • any

声明为any的变量可以赋予任意类型的值

  • tuple

元组类型用来表示已知元素数量和类型的数组,个元素的类型不必相同,对应位置的类型需要一样

let x: [string, number];
x = ['string', 0]; // 正常
x = [0, 'string']; // 报错
  • enum

枚举类型用于定义值集合

enum Color {
    Red,
    Green,
    Blue,
}
let c: Color = Color.Green;
console.log(c); // 1
  • void 标识方法返回值的类型,表示方法没有返回值。
function hello(): void {}
  • never

never是其它类型(包括null和undefined)的子类型,是不会发生的类型。例如,never总是抛出异常或永不返回的异常的函数表达式的返回类型

// 返回 never 的函数终点不可达
function error(message: string): never {
    throw new Error(message);
}

// 推断的返回类型是 never
function fail() {
    return error('Something failed');
}

// 返回 never 的函数终点不可达
function infiniteLoop(): never {
    while (true) {}
}
  • unknown 未知类型,一般在使用后再手动转具体的类型

  • union

联合类型,多种类型之一

string | number; // string 或 number
  • intersection

交叉类型,多种类型合并

{ a: string; } & { b: number; } // => { a: string; b: number }
  • Generics

泛型

interface Backpack<T> {
    add: (obj: T) => void;
    get: () => T;
}

谈谈你对ajax的理解?

ajax是一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。

创建一个ajax的步骤:

首先是创建一个 XMLHttpRequest 对象。

然后在这个对象上使用 open 方法创建一个http 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。

在发起请求前,我们可以为这个对象添加一些信息和监听函数。

比如说我们可以通过setRequestHeader 方法来为请求添加头信息。

我们还可以为这个对象添加一个状态监听函数。

一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,我们可以通过设置监听函数,来处理请求成功后的结果。

当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候我们可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。

这个时候我们就可以通过response 中的数据来对页面进行更新了。当对象的属性和监听函数设置完成后,最后我们调用send 方法来向服务器发起请求,可以传入参数作为发送的数据体

如何创建一个 ajax?

const SERVER_URL = "/server";

let xhr = new XMLHttpRequest();

// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);

// 设置状态监听函数
xhr.onreadystatechange = function () {
  if (this.readyState !== 4) return;

  // 当请求成功时 readyState === 4 && status === 200
  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 封装实现
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;
}

说一下ajax/axios/fetch三者的区别

ajax

ajax是对原生xhr的封装,除此之外还增添了对jsonp的支持。JQuery ajax经过多年的更新维护十分方便了,如果说还有缺点的话,那就是:

  • 本身是针对MVC的编程,不符合现在的前端MVVM的潮流
  • 基于原生的xhr的开发,xhr本身的架构不清晰,已经有fetch的替代方案
  • JQuery整个项目太大,单纯使用ajax却要引入整个JQuery非常不合理(采取个性化打包的方案又不能享受CDN服务)

尽管JQuery对前端的开发工作有着深远的影响,但是随着vue,react新一代的崛起以及ES规范的完善,更多api的更新,JQuery这种大而全的js库,未来的道路会越来越窄

axios

axios本质上也是对原生xhr的封装,只不过是promise的版本,符合最新的ES规范,从其官网上可以看到以下几条特性:

  • 从node.js创建http请求
  • 支持Promise api
  • 客户端支持防止CSRF
  • 提供了一些并发请求的接口

防止CSRF的攻击实现原理:在每个请求都带一个cookie中拿到的key,根据浏览器的同源策略,假冒的网站是拿不到cookie中的key,这样后台轻松辨别出这个请求是否是用户在假冒网站上的误导输入从而采取正确的策略

axios提供了并发的封装,体积较小,是非常使用当下潮流的方式

fetch

fetch号称是ajax的替代品。它的好处有以下几点

  • 语法简洁,更加语义化
  • 基于promise实现,支持async/await
  • 更加接近底层,提供丰富的api
  • 脱离了xhr,是ES规范的新的实现方式

异步请求,低版本 fetch 如何低版本适配?

低版本适配处理

  • 当前浏览器不支持 fetch 时,使用fetch polyfill.
  • 由于 fetch 的底层实现需要用到全局下的 Promise,对于不支持 Promise 的环境还需在全局添加 Promise polyfill

fetch polyfill

  • whatwg-fetch 结合 Promise 使用 XMLHttpRequest 的方式来实现
  • isomorphic-fetch
  • fetch-polyfill2

定时器为什么是不精确的?

首先,我们知道 setInterval 的运行机制

setInterval 属于宏任务,要等到一轮同步代码以及微任务执行完后才会走到宏任务队列,但是前面的任务到底需要多长时间,这个我们是不确定的.

等到宏任务执行,代码会检查 setInterval 是否到了指定时间,如果到了,就会执行 setInterval,如果不到的话,那就要等到下次 EventLoop 重新判断

当然,嗨哟一部分不确定因素,比如 setInterval 的时间戳小于 10ms,那么会被调整至 10ms 执行,因为这是 setInterval 设计以及规定,当然,由于其他任务的影响,这个 10ms 也会不精确.

还有一些物理原因,如果用户使用的设备处于供电状态等,为了节电,浏览器会使用系统定时器,时间间隔将被调整至 16.6ms

为什么使用 setTimeout 实现 setInterval?怎么模拟?

注:setTimeout和setInterval的回调函数,都是经过n毫秒后被添加到队列中,而不是过n毫秒后立即执行

分析

setInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中,只有当目前的执行栈为空的时候,才能去事件队列中取出事件执行。所以可能会出现这样的情况:就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能到间隔一段时间执行的效果

针对 setInterval 的这个缺点,我们可以使用 setTimeout 递归调用来模拟 setInterval,这样我们就确保了只有一个事件结束勒,才会触发下一个定时器事件,这样解决了 setInterval 的问题

实现

思路是使用递归函数,不断地执行 setTimeout 从而达到 setInterval 的效果

  • 简单实现
function mySetInterval(fn, timeout) {
  function interval() {
    fn();
    setTimeout(interval, timeout);
  }
  setTimeout(interval, timeout);
}
  • 进阶版---控制是否继续执行
function mySetInterval(fn, timeout) {
  // 控制器,控制定时器是否继续执行
  var timer = {
    flag: true,
  };

  // 设置递归函数,模拟定时器执行。
  function interval() {
    if (timer.flag) {
      fn();
      setTimeout(interval, timeout);
    }
  }
  // 启动定时器
  setTimeout(interval, timeout);
  // 返回控制器
  return timer;
}

setInterval 需要注意的点

  • setInterval 和 setTimeout 共享一个 ID 池
  • setInterval 需要及时清除,防止内存泄漏
  • 参数 code 传入的值为函数:setInterval('fn()',1500)
  • setInterval 可能不是精确的
  • this的指向问题

setTimeout 有什么缺点,和 requestAnimationFrame 有什么区别?

setTimeout 的缺点

setTimeout 有一个显著的缺陷在于时间是不精确的,setTimeout 只能保证延时或间隔不小于设定的时间。因为它们实际上只是把任务添加到了任务队列中,但是如果前面的任务还没有执行完成,它们必须要等待。

requestAnimationFrame

requestAnimationFrame 是系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销,从而节省系统资源,提高系统性能,改善视觉效果。

requestAnimationFrame 和 setTimeout/setInterval 在编写动画时相比,优点如下:

    1. requestAnimationFrame 不需要设置时间,采用系统时间间隔,能达到最佳的动画效果。
    1. requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成
    1. 当 requestAnimationFrame() 运行在后台标签页或者隐藏的 里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命(大多数浏览器中)。
function step(timestamp) {
  window.requestAnimationFrame(step);
}
window.requestAnimationFrame(step);

说一下 setTimeout 和 setInterval 在内存方面的区别?

setTimeout 表示间隔一段时间之后执行一次调用,而 setInterval 则是每间隔一段时间循环调用,直至 clearInterval 结束。

在内存方面,setTimeout 只需要进入一次队列,不会造成内存溢出,setInterval 因为不计算代码时间,有可能通知执行多次代码,导致内存溢出。

setTimeout/setInterval 实现倒计时如何解决时间偏差的问题?

简单思路

在前端实现中一般会通过 setTimeout 和 setInterval 方法实现一个倒计时效果。但是使用这些方法会存在时间偏差的问题,这是由于 js 的程序执行机制造成的。setTimeout 和 setInterval 的作用是隔一段时间将回调事件加入到事件队列中,因此事件并不是立即执行的,它会等到当前执行栈为空的时候在取出事件执行,因此事件等待执行的时间就是造成误差的原因

一般解决倒计时中的误差的有这样的两种办法:

  • 第一种就是通过前端定时向服务器发送请求获取最新的时间差,以此来校准倒计时时间。但是这样会存在一个很大的问题,就是每隔一秒去请求服务器,这样如果用户多了,服务器就会奔溃---内存占用率很大
  • 第二种方式是前端根据偏差时间来自动调整间隔时间的方式来实现的,这一方式首先是以 setTimeout 递归的方式来实现倒计时,然后通过一个变量来记录已经倒计时的秒数。每一次函数调用的时候,首次按将变量+1,然后根据这个变量和每次的间隔时间,我们就可以计算出此时无偏差时应该显示的时间。然后将当前的真实时间与这个时间相减,这样我们就可以得到时间的偏差大小,因此我们在设置下一个定时器的间隔大小的时候,我们就从间隔时间中减去这个偏差大小,以此来实现由于程序执行所造成的时间误差的纠正。
const interval = 1000;
// 从服务器和活动开始时间计算出的时间差,这里测试用 50000 ms
let ms = 50000;
let count = 0; //记录次数
const startTime = new Date().getTime(); //开始时间
let timeCounter;
if (ms >= 0) {
  timerCounter = setTimeout(countDownStart, interval); //返回一个对象
}
function countDownStart() {
  count++;
  const offset = new Date().getTime() - (startTime + count * interval); //剩余的时间
  let nextTime = interval - offset;
  if (nextTime < 0) {
    nextTime = 0;
  }
  ms -= interval;
  console.log(
    `误差:${offset} ms,下一次执行:${nextTime} ms 后,离活动开始还有:${ms} ms`
  );
  if (ms < 0) {
    clearTimeout(timeCounter); //销毁定时器
  } else {
    timeCounter = setTimeout(countDownStart, nextTime); //重开一个定时器
  }
}

哪些操作会造成内存泄漏?

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

说一说对JSON的理解?

  • JSON 是一种基于文本的轻量级的数据交换格式。

  • 它可以被任何的编程语言读取和作为数据格式来传递。

  • 在项目开发中,我们使用 JSON 作为前后端数据交换的方式。在前端我们通过将一个符合 JSON 格式的数据结构序列化为 JSON 字符串,然后将它传递到后端,后端通过 JSON格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。

  • 因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是我们应该注意的是 JSON和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。

  • 在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,一个是JSON.stringify 函数,通过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,我们可以调用这个函数将数据对象转化为JSON 格式的字符串。

  • 另一个函数 JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当我们从后端接收到 JSON 格式的字符串时,我们可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。

深拷贝如何解决循环引用?

  • 解决办法一:使用 weakmap 解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系
  • 解决办法二: 可以使用 set,发现相同的对象直接赋值,也可以用 Map

先来看看例子:

function deepCopy(obj) {
  const res = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (typeof obj[key] === "object") {
      res[key] = deepCopy(obj[key]);
    } else {
      res[key] = obj[key];
    }
  }
  return res;
}
var obj = {
  a: 1,
  b: 2,
  c: [1, 2, 3],
  d: { aa: 1, bb: 2 },
};
obj.e = obj;
console.log("obj", obj); // 不会报错

const objCopy = deepCopy(obj);
console.log(objCopy); //Uncaught RangeError: Maximum call stack size exceeded

从例子可以看到,当存在循环引用的时候,deepCopy 会报错,栈溢出

  • obj 对象存在循环引用时,打印它时是不会栈溢出
  • 深拷贝 obj 时,才会导致栈溢出

循环应用问题解决

即: 目标对象存在循环应用时报错处理大家都知道,对象的 key 是不能是对象的。

{{a:1}:2}
// Uncaught SyntaxError: Unexpected token ':'
  • 解决办法一:使用weakmap 解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系

这个存储空间,需要可以存储 key-value 形式的数据,且 key 可以是一个引用类型,我们可以选择 weakMap 这种数据结构:

  • 检查 weakMap 中有无克隆过的对象。
  • 有,直接返回
  • 没有,将当前对象作为 key,克隆对象作为 value 进行存储
  • 继续克隆
function isObject(obj) {
  return (typeof obj === "object" || typeof obj === "function") && obj !== null;
}
function cloneDeep(source, hash = new WeakMap()) {
  if (!isObject(source)) return source;
  if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表

  var target = Array.isArray(source) ? [] : {};
  hash.set(source, target); // 新增代码,哈希表设值

  for (var key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      if (isObject(source[key])) {
        target[key] = cloneDeep(source[key], hash); // 新增代码,传入哈希表
      } else {
        target[key] = source[key];
      }
    }
  }
  return target;
}
  • 解决办法二:可以使用 set,发现相同的对象直接赋值,也可以用 Map
const o = { a: 1, b: 2 };
o.c = o;

function isPrimitive(val) {
  return Object(val) !== val;
}
const set = new Set();
function clone(obj) {
  const copied = {};
  for (const [key, value] of Object.entries(obj)) {
    if (isPrimitive(value)) {
      copied[key] = value;
    } else {
      if (set.has(value)) {
        copied[key] = { ...value };
      } else {
        set.add(value);
        copied[key] = clone(value);
      }
    }
  }
  return copied;
}

手写实现 js 中的深拷贝

判断分类

  • null、undefined
  • 正则
  • Date
  • 基础数据类型:数字、boolean、string、symbol、bigint等等
  • 数组和对象,使用 new obj.constructor 构建一个实例
  • 递归
// 拷贝目标
let obj = {
  url: "/api/list",
  method: "GET",
  cache: false,
  timeout: 1000,
  key: Symbol("KEY"),
  big: 10n,
  n: null,
  u: undefined,
  headers: {
    "Content-Type": "application/json",
    post: {
      "X-Token": "xxx",
    },
  },
  arr: [10, 20, 30],
  reg: /^\d+$/,
  time: new Date(),
  fn: function () {
    console.log(this);
  },
  err: new Error("xxx"),
};
obj.obj = obj;
//================ 方法一(最便捷)=================
let newObj = JSON.parse(JSON.stringify(obj));

//================ 方法二(实现数组和对象深拷贝)=================
function deepClone(obj, hash = new WeakMap()) {
  // 弱引用,不要用Map
  // null 和 undefiend 是不需要拷贝的,使用 ==
  if (obj == null) {
    return obj;
  }
  // 正则类型
  if (obj instanceof RegExp) {
    return new RegExp(obj);
  }
  // Date类型
  if (obj instanceof Date) {
    return new Date(obj);
  }
  // 函数是不需要拷贝
  if (typeof obj != "object") return obj;
  // 说明是一个对象类型
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor(); // []  {}
  hash.set(obj, cloneObj);
  for (let key in obj) {
    // in 会遍历当前对象上的属性和__proto__ 上的属性
    // 不拷贝 对象的__proto__ 上的属性
    if (obj.hasOwnProperty(key)) {
      // 如果值还有可能是对象,就继续拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
    // 区分对象和数组 Object.prototype.toString.call
  }
  return cloneObj;
}

let newObj2 = deepClone(obj); // 深拷贝

方法一弊端:

不允许出现套娃操作=>obj.obj = obj;
属性值不能是 BigInt Uncaught TypeError: Do not know how to serialize a BigInt
丢失一些内容:只要属性值是 symbol/undefined/function 这些类型的
还有信息不准确的,例如:正则->空对象 Error 对象->空对象 日期对象->字符串 ...

什么是函数柯里化?实现柯里化函数

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

JS 函数柯里化的优点:

  • 简洁代码:柯里化应用在较复杂的场景中,有简洁代码,可读性高的优点。

  • 参数复用:公共的参数已经通过柯里化预置了。

  • 延迟执行:柯里化时只是返回一个预置参数的新函数,并没有立刻执行,实际上在满足条件后才会执行。

  • 管道式流水线编程:利于使用函数组装管道式的流水线工序,不污染原函数。

const curring = () => {
    let arr = [];
    const add = (...params) => {
        arr = arr.concat(params);
        return add;
    };
    add.toString = () => {
        return arr.reduce((total, item) => {
            return total + item;
        });
    };
    return add;
};

// 经读者反馈,Chrome最新版本在使用 console.log 测试无限柯里化时,已经达不到预期效果,估计与 Chrome console.log 内部实现有关,大家测试时可以用 alert 试试。
let add = curring();
let res = add(1)(2)(3);
alert(res); //->6

add = curring();
res = add(1, 2, 3)(4);
alert(res); //->10

add = curring();
res = add(1)(2)(3)(4)(5);
alert(res); //->15 

手动实现jsonp

jsonp跨域原理

利用script标签的异步加载特性实现给服务端传一个回调函数,服务器返回一个传递过去的回调函数名称的JS代码。即:利用script标签的src属性,通过动态创建一个script标签,指定src属性为跨域的api,那么html会把返回的字符串当作javascript代码来进行解析,如果我们在返回的字符串中使用自定义函数形式包装起来,然后在html中调用自定义函数,即可拿到返回的字符串。

优点

解决跨域问题,能够直接访问响应文本,可用于浏览器与服务器间的双向通信。

缺点

  • JSONP从其他域中加载代码执行,其他域可能不安全
  • 难以确定JSONP请求是否失败。

手动实现jsonp

  1. 挂载回调函数
  2. 将data转化成url字符串的形式
  3. 处理url地址中的回调参数
  4. 创建一个script的标签
  5. 将script标签放到页面中
function jsonp({ url, params, cb }) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script');
    window[cb] = function (data) {
      resolve(data);
      document.body.removeChild(script);
    }
    params = { ...params, cb } // wd=我爱你&cb=show
    let arrs = [];
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`);
    }
    script.src = `${url}?${arrs.join('&')}`;
    document.body.appendChild(script);
  });
}

// 只能发送get请求 不支持post put delete
// 不安全 xss攻击  不采用
jsonp({
  url: 'http://localhost:3000/say',
  params: { wd: '我爱你' },
  cb: 'show'
}).then(data => {
  console.log(data);
});

防抖节流原理、区别以及应用,请用js实现

防抖

原理:在时间被触发n秒之后再执行回调,如果在这n秒内又被触发,则重新计时

适用场景:

  • 搜索框联想场景:防止联想发送请求,只发送最后一次输入
//简易版实现
function debounce(func,wait){
    let timeout;
    return function(){
        const context=this;
        const args=arguments;
        clearTimeout(timeout);
        timeout=setTimeout(() => {
            func.apply(context,args);
        }, wait);
    }
}
//立即执行版实现:有时候希望立刻执行函数,然后等到停止触发n秒后,才可以重新执行。
function debound1(func,wait,immediate){
    let timeout;
    return function(){
        const context=this;
        const args=arguments;
        if(timeout) clearTimeout(timeout);
        if(immediate){
            const callNow=!timeout;
            timeout=setTimeout(() => {
                timeout=null;
            }, wait);
            if(callNow) func.apply(context,args);
        }else{
            timeout=setTimeout(() => {
                func.apply(context,args);
            }, wait);
        }
    }
}

//返回值版实现
//func函数可能会有返回值,所以需要返回函数结果,但是当immediate为false的时候,因为使用了setTimeout,我们将func.apply(context,args)的返回值赋给变量,最后在return的时候,值将会一直是undefined,所以只在immediate为true的时候返回函数的执行结果
function debounce2(func,wait,immediate){
    let timeout,result;
    return function(){
        const context=this;
        const args=arguments;
        if(timeout) clearTimeout(timeout);
        if(immediate){
            const callNow=!timeout;
            timeout=setTimeout(() => {
                timeout=null;
            }, wait)
            if (callNow) result=func.apply(context,args);
        }else{
            timeout=setTimeout(() => {
                func.apply(context,args);   
            }, wait);
        }
        return result;
    }
}

节流:

原理: 规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效

适用场景:

  • 拖拽场景:固定时间内只执行一次,防止高频次触发位置变动
  • 缩放场景:监控浏览器resize
  • 滚动
//使用时间戳实现:使用时间戳,当触发事件发生的时候,我们取出当前的时间戳,然后减去之前的时间戳(最开始设值为0),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前时间戳,如果小于,就不执行 

 function throttle(func,wait){
     let context,args;
     let previous=0;
     return function(){
         let now=+new Date();
         context=this;
         args=arguments;
         if(now-previous > wait){
             func.apply(context,args);
             previous=now
         }
     }
 }
//使用定时器实现:当触发事件的时候,我们设置一个定时器,在触发事件的时候,如果定时器存在,就不执行,知道定时器执行,然后执行函数,清空定时器,这样就可以设置下定时器
function throttle1(func,wait){
    let timeout;
    return function(){
        const context=this;
        const args=arguments;
        if(!timeout){
            timeout=setTimeout(() => {
                timeout=null;
                func.apply(context,args);
            }, wait);
        }
    }
}

//区别: 节流不管事件触发多频繁保证在一定时间内一定会执行一次函数。防抖是只在最后一次事件触发后才会执行一次函数
// flag版本
const throttle2=(func,wait)=>{
    let flag=true;
    return function(...args){
        if(!flag) return;
        flag=flase;
        setTimeout(() => {
            func.apply(this,args)
            flag=true;
        }, wait);
    } 
}

说一下栈和堆的区别,垃圾回收时栈和堆的区别?

一、栈和堆的区别

栈:其操作系统自动分配释放,存放函数的参数值和局部变量的值等。其操作方式类似于数据结构中的栈。简单的理解就是当定义一个变量的时候,计算机会在内存中开辟一块存储空间来存放这个变量的值,这块空间叫做栈,然而栈中一般存放的是基本数据类型,栈的特点就是先进后出(或者后进先出)

堆:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。其实在堆中一般存放变量的是一些对象类型

  • 1.存储大小

栈内存的存储大小是固定的,申请时由系统自动分配内存空间,运行的效率比较快,但是因为存储的大小固定,所以容易存储的大小超过存储的大小,导致溢栈

堆内存的存储的值是大小不定,是由程序员自己申请并指明大小。因为堆内存是new分配的内存,所以运行的效率会比较低

  • 2.存储对象

栈内存存储的是基础数据类型,并且是按值访问的,因为栈是一块连续的内存区域,以后进先出的原则存储调用的,所以是连续存储的

堆内存是向高地址扩展的数据结构,是不连续的内存区域,系统也是用链表来存储空闲的内存地址,所以是不连续的。因为是记录的内存地址,所以获取是通过引用,存储的是对象居多

  • 3.回收

栈的回收是系统控制实现的

堆内存的回收是人为控制的,当程序结束后,系统会自动回收

二、垃圾回收栈和堆的区别

  • 栈内存中的数据只要结束,则直接回收
  • 堆内存中的对象回收标准是否可达,在V8中对象先分配到新生代的From中,如果不可达直接释放,如果可达,就复制到TO中,然后将TO和From互换。当多次复制后依然没有回收,则放入老生代中,进行标记回收。之后将内存碎片进行整合放到一端。

说一下import的原理,和require的不同之处在哪儿?

import原理(实际上就是ES6 module的原理)

  1. 简单来说就是闭包作用
  2. 为了创建Module的内部作用域,会调用一个包装函数
  3. 包装函数的返回值也就是Module向外公开的API,也就是所有export出去的变量
  4. import是拿到module导出变量的引用

与require的不同之处

  1. CommonJS模块输出的是一个值的拷贝,ES6模块输出的值的引用
  2. CommonJS模块是运行时加载,ES6模块是编译时输出接口

CommonJS是运行时加载对应模块,一旦输出一个值,即使模块内部对其做出改变,也不影响输出值。而ES6模块不同,import导入是在JS引擎对脚本静态分析时确定,获取到的是一个只读引用。等脚本引擎运行时,会根据这个引用去对应模块中的取值,所以引用对应的值改变的时候,其导入的值也会发生改变