JavaScript面试的完美指南(开发者视角)

为了说明 JS 面试的复杂性,首先,请尝试给出如下结果:javascript

onsole.log(2.0 == “2” == new Boolean(true) == “1”)

十有八九的会给出false, 其实运行结果是true,缘由请看 这里html

1) 理解 JS 函数

函数是 JavaScript 的精华,是 JS 一等公民。JS 函数不只仅是一个普通的函数,与其余语言不一样,JS 函数能够赋值给变量,做为参数传递给另外一个函数,也能够从另外一个函数返回。java

console.log(square(5));
/* ... */
function square(n) { return n * n; }

觉得代码很简单,你们应该都知道会打印:25。接着看一个:node

console.log(square(5));
 
var square = function(n) { 
  return n * n; 
}

乍一看,你可能会忍不住说也打印了 25。但很不幸,会报错:git

TypeError: square is not a function

在 JavaScript 中,若是将函数定义为变量,变量名将被提高,是 JS 执行到它的定义才能被访问。es6

你可能在一些代码中频繁的见到以下代码。github

var simpleLibrary = function() {
   var simpleLibrary = {
        a,
        b,
        add: function(a, b) {
            return a + b;
        },
        subtract: function(a, b) {
            return a - b;   
        }
   }
  return simpleLibrary;
}();

为何会作这种奇怪的事情? 这是由于一个函数变量中变量和函数被分装,能够避免全局变量污染。 JQueryLodash 的库采用这种技术提供 $_面试

2) 理解 bind、apply 和 call

你可能在全部经常使用库中看到过这三个函数。它们容许局部套用, 咱们能够把功能组合到不一样的函数。一个优秀的js开发者能够随时告诉你关于这三个函数。ajax

基本上,这些是改变行为以实现某些功能的原型方法,根据 JS 开发人员 Chad 的说法,用法以下:正则表达式

但愿使用某个上下文调用该函数,请使用 .bind() ,这在事件中颇有用。 若是要当即调用函数,请使用.call().apply(),并修改上下文。

举例说明

让咱们看看上面的陈述是什么意思! 假设你的数学老师要求你建立一个库并提交。你写了一个抽象的库,它能够求出圆的面积和周长:

var mathLib = {
    pi: 3.14,
    area: function(r) {
        return this.pi * r * r;
    },
    circumference: function(r) {
        return 2 * this.pi * r;
    }
};

提交后,老师调用了它:

mathLib.area(2);
12.56

老师发现他给你要求是 pi 精确到小数点后 5 位数而你只精确到 2 位, 如今因为最后期限已过你没有机会提交库。 这里 JS的 call 函数能够帮你, 只须要调用你的代码以下:

mathLib.area.call({pi: 3.1.159}, 2)

它会动态地获取新的 pi 值,结果以下:

12.56636

这时,注意到 call 函数具备两个参数:

  • Context
  • 函数参数

area 函数中, 上下文是对象被关键词 this 代替,后面的参数做为函数参数被传递。 以下:

var cylinder = {
    pi: 3.14,
    volume: function(r, h) {
        return this.pi * r * r * h;
    }
};

调用方式以下:

cylinder.volume.call({pi: 3.14159}, 2, 6);
75.39815999999999

Apply 相似,只是函数参数做为数组传递。

cylinder.volume.apply({pi: 3.14159}, [2, 6]);
75.39815999999999

若是你会使用 call 你基本就会用 apply 了,反之亦然, 那 bind 的用法又是如何呢 ?

bind 将一个全新的 this 注入到指定的函数上,改变 this 的指向, 使用 bind 时,函数不会像 callapply 当即执行。

var newVolume = cylinder.volume.bind({pi: 3.14159});
newVolume(2,6); // Now pi is 3.14159

bind 用途是什么?它容许咱们将上下文注入一个函数,该函数返回一个具备更新上下文的新函数。这意味着这个变量将是用户提供的变量,这在处理 JavaScript 事件时很是有用。

3) 理解 js 做用域(闭包)

JavaScript 的做用域是一个潘多拉盒子。从这一个简单的概念中,就能够构造出数百个难回答的面试问题。有三种做用域:

  • 全局做用域
  • 本地/函数做用域
  • 块级做用域(ES6引进)

全局做用域事例以下:

x = 10;
function Foo() {
  console.log(x); // Prints 10
}
Foo()

函数做用域生效当你定义一个局部变量时:

pi = 3.14;
function circumference(radius) {    
     pi = 3.14159;
     console.log(2 * pi * radius); // 打印 "12.56636" 不是 "12.56"
}
circumference(2);

ES16 标准引入了新的块做用域,它将变量的做用域限制为给定的括号块。

var a = 10;

function Foo() {
  if (true) {
    let a = 4;
  }

  alert(a); // alerts '10' because the 'let' keyword
}
Foo();

函数和条件都被视为块。以上例子应该弹出 4,由于 if 已执行。但 是ES6 销毁了块级变量的做用域,做用域进入全局。

如今来到神奇的做用域,可使用闭包来实现,JavaScript 闭包是一个返回另外一个函数的函数。

若是有人问你这个问题,编写一个输入一个字符串并逐次返回字符。 若是给出了新字符串,则应该替换旧字符串,相似简单的一个生成器。

function generator(input) {
  var index = 0;
  return {
    next: function() {
      if (index < input.lenght) {
        return input[index -1];
      }
      return "";
    }
  }
}

执行以下:

var mygenerator = generator("boomerang");
mygenerator.next(); // returns "b"
mygenerator.next() // returns "o"
mygenerator = generator("toon");
mygenerator.next(); // returns "t"

在这里,做用域扮演着重要的角色。闭包是返回另外一个函数并携带数据的函数。上面的字符串生成器适用于闭包。index 在多个函数调用之间保留,定义的内部函数能够访问在父函数中定义的变量。这是一个不一样的做用域。若是在第二级函数中再定义一个函数,它能够访问全部父级变量。

4) this (全局域、函数域、对象域)

在 JavaScript 中,咱们老是用函数和对象编写代码, 若是使用浏览器,则在全局上下文中它引用 window 对象。 个人意思是,若是你如今打开浏览器控制台并输入如下代码,输出结果为 true

this === window;

当程序的上下文和做用域发生变化时,this 也会发生相应的变化。如今观察 this 在一个局部上下文中:

function Foo(){
  console.log(this.a);
}
var food = {a: "Magical this"};
Foo.call(food); // food is this

思考一下,如下输出的是什么:

function Foo(){
    console.log(this); // 打印 {}?
}

由于这是一个全局对象,记住,不管父做用域是什么,它都将由子做用域继承。打印出来是 window 对象。上面讨论的三个方法实际上用于设置这个对象。

如今,this 的最后一个类型,在对象中的 this, 以下:

var person = {
    name: "Stranger",
    age: 24,
    get identity() {
        return {who: this.name, howOld: this.age};
    }
}

上述使用了 getter 语法,这是一个能够做为变量调用的函数。

person.identity; // returns {who: "Stranger", howOld: 24}

此时,this 其实是指对象自己。正如咱们前面提到的,它在不一样的地方有不一样的表现。

5) 理解对象 (Object.freeze, Object.seal)

一般对象的格式以下:

var marks = {physics: 98, maths:95, chemistry: 91};

它是一个存储键、值对的映射。 javascript 对象有一个特殊的属性,能够将任何东西存储为一个值。这意味着咱们能够将一个列表、另外一个对象、一个函数等存储为一个值。

能够用以下方式来建立对象:

var marks = {};
var marks = new Object();

可使用 JSON.stringify() 将一个对象转制成字符串,也能够用 JSON.parse 在将其转成对象。

// returns "{"physics":98,"maths":95,"chemistry":91}"
JSON.stringify(marks);
// Get object from string
JSON.parse('{"physics":98,"maths":95,"chemistry":91}');

使用 Object.keys 迭代对象:

var highScere = 0;

for (i of Object.keys(marks)) {
  if (marks[i] > highScore)
    highScore = marks[i];
}

Object.values 以数组的方式返回对象的值。

对象上的其余重要函数有:

  • Object.prototype(object)
  • Object.freeze(function)
  • Object.seal(function)

Object.prototype 上提供了许多应用上相关的函数,以下:

Object.prototype.hasOwnProperty 用于检查给定的属性/键是否存在于对象中。

marks.hasOwnProperty("physics"); // returns true
marks.hasOwnProperty("greek"); // returns false

Object.prototype.instanceof 判断给定对象是不是特定原型的类型。

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
var newCar = new Car('Honda', 'City', 2007);
console.log(newCar instanceof Car); // returns true

使用 Object.freeze 能够冻结对象,以便不能修改对象现有属性。

var marks = {physics: 98, maths:95, chemistry: 91};
finalizedMarks = Object.freeze(marks);
finalizedMarks["physics"] = 86; // throws error in strict mode
console.log(marks); // {physics: 98, maths: 95, chemistry: 91}

在这里,试图修改冻结后的 physics 的值,但 JavaScript不容许这样作。咱们可使用 Object.isFrozen 来判断,给定对象是否被冻结:

Object.isFrozen(finalizedMarks); // returns true

Object.sealObject.freeze 略有不一样。 Object.seal() 方法封闭一个对象,阻止添加新属性并将全部现有属性标记为不可配置。当前属性的值只要可写就能够改变。

var marks = {physics: 98, maths:95, chemistry: 91};
Object.seal(marks);
delete marks.chemistry; // returns false as operation failed
marks.physics = 95; // Works!
marks.greek = 86; // Will not add a new property

一样, 可使用 Object.isSealed 判断对象是否被密封。

Object.isSealed(marks); // returns true

在全局对象函数上还有许多其余重要的函数/方法,在这里找到他们。

代码部署后可能存在的BUG无法实时知道,过后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给你们推荐一个好用的BUG监控工具 Fundebug

6) 理解原型继承

在传统 JavaScript 中,有一种假装的继承概念,它是经过使用原型技术来实现的。在ES五、ES6中看到使用 new 的语法只是底层原型OOP的语法糖。建立类是使用 JavaScript 中的函数完成的。

var animalGroups = {
  MAMMAL: 1,
  REPTILE: 2,
  AMPHIBIAN: 3,
  INVERTEBRATE: 4
};
function Animal(name, type) {
  this.name = name;
  this.type = type;
}
var dog = new Animal("dog", animalGroups.MAMMAL);
var crocodile = new Animal("crocodile", animalGroups.REPTILE);

这里咱们为类建立对象(使用 new 关键字),可使用以下方式对类追加方法:

Animal.prototype.shout = function() {
  console.log(this.name+'is'+this.sound+'ing...');
}

这里你可能会有疑问。类中并没 sound 属性。是的,它打算由继承了上述类的子类传递。

JavaScript中, 以下实现继承:

function Dog(name, type) {
Animal.call(this, name, type);
this.sound = 'bow';
}

我定义了一个更具体的函数,叫作 Dog。在这里,为了继承 Animal 类,我须要call传递this和其余参数。使用以下方式来实例化一只德国牧羊犬

var pet = Dog("德国牧羊犬", animalGroups.MAMMAL);
console.log(pet); // returns Dog {name: "德国牧羊犬", type: 1, sound: "bow"}

咱们没有在子函数中分配 nametype 属性,咱们调用的是超级函数 Animal 并设置相应的属性。pet 具备父类的属性(name、type)。可是方法呢。他们也继承的吗? 来看看:

pet.shout(); // Throws error

为何会这样? 之因此发生这种状况,是由于没有指定让 JavaScript来继承父类方法。 如何解决?

// Link prototype chains
Dog.prototype = Object.create(Animal.prototype);
var pet = new Dog("germanShepard", animalGroups.MAMMAL);
// Now shout method is available
pet.shout(); // 德国牧羊犬 bowing...

如今可使用 shout 方法。 咱们可使用 object.constructor 函数检查 JavaScript 中给定对象的类 来看看 pet 是什么类:

pet.constructor; // returns Animal

这是模糊的,Animal 是一个父类。可是 pet 究竟是什么类型的呢? pet 应该是 Dog 的类型。之因此是 Animal 类型,是由于 Dog 类的构造函数:

Dog.prototype.constructor; // returns Animal

它是 Animal 类型的。咱们应该将它设置为 Dog 自己,这样类的全部实例(对象)才能给出正确的类名。

Dog.prototype.constructor = Dog;

关于原型继承, 咱们应该记住如下几条:

  • 类属性使用 this 绑定
  • 类方法使用 prototype 对象来绑定
  • 为了继承属性, 使用 call 函数来传递 this
  • 为了继承方法, 使用 Object.create 链接父和子的原型
  • 始终将子类构造函数设置为自身,以得到其对象的正确类型

7)理解 callback 和 promise

回调是在 I/O 操做完成后执行的函数。一个耗时的I/O操做会阻塞代码, 所以在Python/Ruby不被容许。可是在 JavaScript中,因为容许异步执行,咱们能够提供对异步函数的回调。这个例子是由浏览器到服务器的AJAX(XMLHettpRequest)调用,由鼠标、键盘事件生成。以下:

function reqListener () {
  console.log(this.responseText);
}

var req = new XMLHttpRequest();
req.addEventListener("load", reqListener);
req.open("GET", "http://www.example.org/example.txt");
req.send();

这里的 reqListener 是一个回调函数,当成功响应 GET 请求时将执行该回调函数。

Promise 是回调函数的优雅的封装, 使得咱们优雅的实现异步代码。在如下给出的这篇文章中讨论了不少 promise,这也是在 JS 中应该知道的重要部分。

Writing neat asynchronous Node JS code with Promises

8)理解正则表达

正则表达式有许多应用地方,处理文本、对用户输入执行规则等。JavaScript 开发人员应该知道如何执行基本正则表达式并解决问题。Regex 是一个通用概念,来看看如何从 JS 中作到这一点。

建立正则表达式,有以下两种方式:

var re = /ar/;
var re = new RegExp('ar');

上面的正则表达式是与给定字符串集匹配的表达式。定义正则表达式以后,咱们能够尝试匹配并查看匹配的字符串。可使用 exec 函数匹配字符串:

re.exec("car"); // returns ["ar", index: 1, input: "car"]
re.exec("cab"); // returns null

有一些特殊的字符类容许咱们编写复杂的正则表达式。RegEx 中有许多类型的元素,其中一些以下:

  • 字符正则:\w-字母数字, \d- 数字, \D- 没有数字
  • 字符类正则:[x-y] x-y区间, [^x] 没有x
  • 数量正则:+ 至少一个、? 没或多个、* 多个
  • 边界正则,^ 开始、$ 结尾

例子以下:

/* Character class */

var re1 = /[AEIOU]/;
re1.exec("Oval"); // returns ["O", index: 0, input: "Oval"]
re1.exec("2456"); // null
var re2 = /[1-9]/;
re2.exec('mp4'); // returns ["4", index: 2, input: "mp4"]

/* Characters */

var re4 = /\d\D\w/;
re4.exec('1232W2sdf'); // returns ["2W2", index: 3, input: "1232W2sdf"]
re4.exec('W3q'); // returns null

/* Boundaries */

var re5 = /^\d\D\w/;
re5.exec('2W34'); // returns ["2W3", index: 0, input: "2W34"]
re5.exec('W34567'); // returns null
var re6 = /^[0-9]{5}-[0-9]{5}-[0-9]{5}$/;
re6.exec('23451-45242-99078'); // returns ["23451-45242-99078", index: 0, input: "23451-45242-99078"]
re6.exec('23451-abcd-efgh-ijkl'); // returns null

/* Quantifiers */

var re7 = /\d+\D+$/;
re7.exec('2abcd'); // returns ["2abcd", index: 0, input: "2abcd"]
re7.exec('23'); // returns null
re7.exec('2abcd3'); // returns null
var re8 = /<([\w]+).*>(.*?)<\/\1>/;
re8.exec('<p>Hello JS developer</p>'); //returns  ["<p>Hello JS developer</p>", "p", "Hello JS developer", index: 0, input: "<p>Hello JS developer</p>"]

有关 regex 的详细信息,能够看 这里

除了 exec 以外,还有其余函数,即 matchsearchreplace,可使用正则表达式在另外一个字符串中查找字符串,可是这些函数在字符串自己上使用。

"2345-678r9".match(/[a-z A-Z]/); // returns ["r", index: 8, input: "2345-678r9"]
"2345-678r9".replace(/[a-z A-Z]/, ""); // returns 2345-6789

Regex 是一个重要的主题,开发人员应该理解它,以便轻松解决复杂的问题。

9)理解 map、reduce 和 filter

函数式编程是当今的一个热门讨论话题。许多编程语言都在新版本中包含了函数概念,好比 lambdas(例如:Java >7)。在 JavaScrip t中,函数式编程结构的支持已经存在很长时间了。咱们须要深刻学习三个主要函数。数学函数接受一些输入和返回输出。纯函数都是给定的输入返回相同的输出。咱们如今讨论的函数也知足纯度。

map

map 函数在 JavaScript 数组中可用,使用这个函数,咱们能够经过对数组中的每一个元素应用一个转换函数来得到一个新的数组。map 通常语法是:

arr.map((elem){
    process(elem)
    return processedValue
}) // returns new array with each element processed

假设,在咱们最近使用的串行密钥中输入了一些不须要的字符,须要移除它们。此时可使用 map 来执行相同的操做并获取结果数组,而不是经过迭代和查找来删除字符。

var data = ["2345-34r", "2e345-211", "543-67i4", "346-598"];
var re = /[a-z A-Z]/;
var cleanedData = data.map((elem) => {return elem.replace(re, "")});
console.log(cleanedData); // ["2345-34", "2345-211", "543-674", "346-598"]

map 接受一个做为参数的函数, 此函数接受一个来自数组的参数。咱们须要返回一个处理过的元素, 并应用于数组中的全部元素。

reduce

reduce 函数将一个给定的列表整理成一个最终的结果。经过迭代数组执行相同的操做, 并保存中间结果到一个变量中。这里是一个更简洁的方式进行处理。js 的 reduce 通常使用语法以下:

arr.reduce((accumulator,
           currentValue,
           currentIndex) => {
           process(accumulator, currentValue)
           return intermediateValue/finalValue
}, initialAccumulatorValue) // returns reduced value

accumulator 存储中间值和最终值。currentIndexcurrentValue分别是数组中元素的 index 和 value。initialAccumulatorValueaccumulator 初始值。

reduce 的一个实际应用是将一个数组扁平化, 将内部数组转化为单个数组, 以下:

var arr = [[1, 2], [3, 4], [5, 6]];
var flattenedArray = [1, 2, 3, 4, 5, 6];

咱们能够经过正常的迭代来实现这一点,可是使用 reduce,代码会更加简洁。

var flattenedArray = arr.reduce((accumulator, currentValue) => {
    return accumulator.concat(currentValue);
}, []); // returns [1, 2, 3, 4, 5, 6]

filter

filtermap 更为接近, 对数组的每一个元素进行操做并返回另一个数组(不一样于 reduce 返回的值)。过滤后的数组可能比原数组长度更短,由于经过过滤条件,排除了一些咱们不须要的。

filter 语法以下:

arr.filter((elem) => {
   return true/false
})

elem 是数组中的元素, 经过 true/false 表示过滤元素保存/排除。假设, 咱们过滤出以 t 开始以 r 结束的元素:

var words = ["tiger", "toast", "boat", "tumor", "track", "bridge"]
var newData = words.filter((str) => {
    return str.startsWith('t') && str.endsWith('r');
})
newData // (2) ["tiger", "tumor"]

当有人问起JavaScript的函数编程方面时,这三个函数应该信手拈来。 如你所见,原始数组在全部三种状况下都没有改变,这证实了这些函数的纯度。

10) 理解错误处理模式

这是许多开发人员最不关心的 JavaScript。 我看到不多有开发人员谈论错误处理, 一个好的开发方法老是谨慎地将 JS 代码封装装在 try/catch 块周围。

在 JavaScript中,只要咱们随意编写代码,就可能会失败,若是所示:

$("button").click(function(){
    $.ajax({url: "user.json", success: function(result){
        updateUI(result["posts"]);
    }});
});

这里,咱们陷入了一个陷阱,咱们说 result 老是 JSON 对象。但有时服务器会崩溃,返回的是 null 而不是 result。在这种状况下,null["posts"] 将抛出一个错误。正确的处理方式多是这样的:

$("button").click(function(){
    $.ajax({url: "user.json", success: function(result){
    
      try {     
        updateUI(result["posts"]);
       }
      catch(e) {
        // Custom functions
        logError();
        flashInfoMessage();      
      }
    }});
});

logError 函数用于向服务器报告错误。flashInfoMessage 是显示用户友好的消息,如“当前不可用的服务”等。

Nicholas 说,当你以为有什么意想不到的事情将要发生时,手动抛出错误。区分致命错误和非致命错误。以上错误与后端服务器宕机有关,这是致命的。在那里,应该通知客户因为某种缘由服务中断了。

在某些状况下,这可能不是致命的,但最好通知服务器。为了建立这样的代码,首先抛出一个错误,, 从 window 层级捕捉错误事件,而后调用API将该消息记录到服务器。

reportErrorToServer = function (error) {
  $.ajax({type: "POST", 
          url: "http://api.xyz.com/report",
          data: error,
          success: function (result) {}
  });
}
// Window error event
window.addEventListener('error', function (e) {
  reportErrorToServer({message: e.message})
})}
function mainLogic() {
  // Somewhere you feel like fishy
  throw new Error("user feeds are having fewer fields than expected...");
}

这段代码主要作三件事:

  • 监听window层级错误
  • 不管什么时候发生错误,都要调用 API
  • 在服务器中记录

你也可使用新的 Boolean 函数(es5,es6)在程序以前监测变量的有效性而且不为null、undefined

if (Boolean(someVariable)) {
// use variable now
} else {
    throw new Error("Custom message")
}

始终考虑错误处理是你本身, 而不是浏览器。

其余(提高机制和事件冒泡)

以上全部概念都是 JavaScript 开发人员的须要知道基本概念。有一些内部细节须要知道,这些对你会有很在帮助。 这些是JavaScript引擎在浏览器中的工做方式,什么是提高机制和事件冒泡?

提高机制

变量提高是 在代码执行过程当中将声明的变量的做用域提高到全局做用哉中的一个过程,如:

doSomething(foo); // used before
var foo; // declared later

当在 Python 这样的脚本语言中执行上述操做时,它会抛出一个错误,由于须要先定义而后才能使用它。尽管 JS 是一种脚本语言,但它有一种提高机制,在这种机制中,JavaScript VM 在运行程序时作两件事:

  • 首先扫描程序,收集全部的变量和函数声明,并为其分配内存空间
  • 经过填充分配的变量来执行程序, 没有分配则填充 undefined

在上面的代码片断中,console.log 打印 “undefined”。 这是由于在第一次传递变量 foo 被收集。 JS 虚拟机 查找为变量 foo 定义的任何值。 这种提高可能致使许多JavaScript 在某些地方抛出错误,和另外地方使用 undefined

学习一些 例子 来搞清楚提高。

事件冒泡

如今事件开始冒泡了! 根据高级软件工程师 Arun P的说法:

“当事件发生在另外一个元素内的元素中时,事件冒泡和捕获是 HTML DOM API 中事件传播的两种方式,而且这两个元素都已为该事件注册了处理程序,事件传播模式肯定元素接收事件的顺序。“

经过冒泡,事件首先由最内部的元素捕获和处理,而后传播到外部元素。对于捕获,过程是相反的。咱们一般使用addEventListener 函数将事件附加处处理程序。

addEventListener("click", handler, useCapture=false)

useCapture 是第三个参数的关键词, 默认为 false。所以, 冒泡模式是事件由底部向上传递。 反之, 这是捕获模式。

冒泡模式:

<div onClick="divHandler()">
    <ul onClick="ulHandler">
        <li id="foo"></li>
    </ul>
</div>
<script>
function handler() {
 // do something here
}
function divHandler(){}
function ulHandler(){}
document.getElementById("foo").addEventListener("click", handler)
</script>

点击li元素, 事件顺序:

handler() => ulHandler() => divHandler()

在图中,处理程序按顺序向外触发。相似地,捕获模型试图将事件从父元素向内触发到单击的元素。如今更改上面代码中的这一行。

document.getElementById("foo").addEventListener("click", handler, true)

事件顺序:

divHandler => ulHandler() => handler()

你应该正确地理解事件冒泡(不管方向是指向父节点仍是子节点),以实现用户界面(UI),以免任何不须要的行为。

这些是 JavaScrip t中的基本概念。正如我最初提到的,除了工做经验和知识以外,准备有助理于你经过 JavaScript 面试。始终保持学习。留意最新的发展(第六章)。深刻了解JavaScript的各个方面,如 V6 引擎、测试等。最后,没有掌握数据结构和算法的面试是不成功的。Oleksii Trekhleb 策划了一个很棒的 git repo,它包含了全部使用 JS 代码的面试准备算法。

关于Fundebug

Fundebug专一于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。自从2016年双十一正式上线,Fundebug累计处理了

10亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎你们免费试用

相关文章
相关标签/搜索