前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【JS】246-如何在JavaScript面试中过五关斩六将?

【JS】246-如何在JavaScript面试中过五关斩六将?

作者头像
pingan8787
发布2019-07-25 11:04:39
1.2K0
发布2019-07-25 11:04:39
举报
文章被收录于专栏:前端自习课前端自习课

JavaScript 面试不容易。我觉得难,你也觉得不容易,大家的意见不谋而合。在 JavaScript 面试中被问问题的概率通常很高。那么该如何破解 JS 面试?突破口在哪儿?本文旨在通过学习基本概念来指导所有有志向的 JavaScript 开发者加深他们的 JS 知识。

以下为译文:

应对 JS 面试,本文至少算是必备常识。如果我是候选人,我会争取很好地掌握这些概念。如果我是面试官,我认为只有掌握这些重要概念的开发者才能走得更远。

本文对于 JS 开发者来说,是入门级指南而非资深级。不同的人有责任为更艰难的面试做好准备。面试者还需要记住,面试问题也可以源自他们的工作领域和技术(例如:React JS、WebPack、Node JS 等)。本文将介绍基本的 JS 元素,只有非常精通它们的人才能被称为一名优秀的 JS 开发者。优秀的 JS 开发者可以是优秀的 React 开发者,反之不一定成立。遗憾的是,JS 因衍生出了大量不规范的脚本(部分属实)而常常被人诟病。JS 协助开发者实现产品功能,满意度较高。编程也是趣事。很少有像 John Resig(jQuery 创建者)、Brendan Eich(JS 创建者)和 Lars Bak(谷歌 Chrome 团队)这么伟大的 JavaScript 程序员,能够完全理解这种语言。成功的 JS 程序员常常查阅代码库中基本的 JS 代码。许多人认为很难找到一名优秀的 JS 开发者。

“虚拟机就像一种奇怪的野兽。我们没有完美的解决方案,而是力争优化至‘最佳点’。而优化的方法有很多。这是一场漫长的游戏,你不会倦怠的。” ——Lars Vak ,Google

为了说明 JS 面试的复杂性,看下面的 JS 表述,试着第一反应说出结果。

代码语言:javascript
复制
console.log(2.0 == “2” == new Boolean(true) == “1”)

90%的人认为输出 false。但答案是 true。为什么?往下看。

JavaScript 很难。如果面试官很聪明的避开类似以上的问题,我们就无能为力了。但是我们能做什么呢?深入学习这11个基本要素,有助于应对 JS 面试。

1. 理解 JS 函数

函数是 JS 的精华。它们是第一类公民。如果没有深入理解函数,你的 JS 知识就像一盘散沙。JS 函数不仅仅是一个普通函数。与其他编程语言不同,函数可以赋值给变量,可以作为参数传递给另一个函数,也可以从另一个函数中返回。因此,函数是 JS 的第一类公民。

这里就不赘述函数的概念了,但你知道的吧?函数就类似这样!

代码语言:javascript
复制
console.log(square(5));
/* ... */
function square(n) { return n * n; }

这段代码的执行结果是25。正确!再看下面的代码:

代码语言:javascript
复制
console.log(square(5));

var square = function(n) { 
  return n * n; 
}

乍一看,你可能会说这也输出25。错!相反,第一行报错了:

代码语言:javascript
复制
TypeError: square is not a function

在 JS 中,如果将函数定义为变量,这函数名将被挂起,只有当 JS 执行到它的定义位置时才能访问到。出乎意料了吗?

先不管它。你可能在某些代码中经常看到这种语法。

代码语言:javascript
复制
var simpleLibrary = function() {
   var simpleLibrary = {
        a,
        b,
        add: function(a, b) {
            return a + b;
        },
        subtract: function(a, b) {
            return a - b;   
        }
   }
  return simpleLibrary;
}();

是不是有点费解?它是一个函数变量,里面的变量和函数不会污染到全局作用域。从 jQuery 到 Lodash 之类的库都用 $etc 表示该用法。

在这里我想说的是“学好函数”。在使用函数的过程中可能会有很多小陷阱。浏览 Mozilla 介绍的函数用法吧,写的很好(https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions)。

2. 掌握 bind, apply 和 call 的用法

这些函数在所有知名的库中可能都可以看到。它们提供了柯里化的方法,可通过编写不同的函数来实现功能。优秀的 JavaScript 开发者可以随时说出这三个函数的用法。

本质上,它们是函数的原型方法,通过改变行为来实现某些功能。根据 JS 开发者 Chad 的说法,它们的用法是这样的:

当希望延迟调用带有特定上下文的函数时,使用 .bind(),这在事件中很有用。当希望立刻调用函数时,使用 .call() 或 .apply(),同时会修改上下文。

call 函数拯救了我!

让我们看看上面论述代表什么意思。假设你的数学老师要求你创建一个库并提交它。你编写了一个计算圆的面积与周长的抽象的库。

代码语言:javascript
复制
var mathLib = {
    pi: 3.14,
    area: function(r) {
        return this.pi * r * r;
    },
    circumference: function(r) {
        return 2 * this.pi * r;
    }
};

你把代码库提交给老师。现在运行调用该数学库的代码。

代码语言:javascript
复制
mathLib.area(2);
12.56

正要提交第二个代码示例时,你恍然发觉老师要求 pi 常数精确到小数点后五位。噢天哪!你是用了3.14不是3.14159。但现在截止日期已过,不能再提交代码了。JS 的 call 函数拯救了你。只需这样:

代码语言:javascript
复制
mathLib.area.call({pi: 3.14159}, 2);

那么它在执行中会取新的pi值。输出结果是:

代码语言:javascript
复制
12.56636

如此,老师会很欣慰。你会发现 call 函数接收了两个参数:

  • 上下文
  • 函数参数

上下文是在函数体内替换 this 的对象。接着,参数会通过函数的参数传入。例如:

代码语言:javascript
复制
var cylinder = {
    pi: 3.14,
    volume: function(r, h) {
        return this.pi * r * r * h;
    }
};

call 是这样用的:

代码语言:javascript
复制
cylinder.volume.call({pi: 3.14159}, 2, 6);
75.39815999999999

发现了吗?函数参数是在上下文对象后,作为参数传递的。

Apply 是完全相同的用法,只是函数参数是以列表的形式传递。

代码语言:javascript
复制
cylinder.volume.apply({pi: 3.14159}, [2, 6]);
75.39815999999999

如果你了解 call 函数,那你就也了解 apply 函数,反之亦然。那什么是 bind 函数?

Bind 将一个全新的 this 赋给指定的函数。Bind 与 call 或 apply 不同,bind 情况下,函数不会立即执行。

代码语言:javascript
复制
var newVolume = cylinder.volume.bind({pi: 3.14159}); // This is not instant call
// After some long time, somewhere in the wild
newVolume(2,6); // Now pi is 3.14159

Bind 函数有什么用途?它提供了给函数传入上下文的方法,并返回带有更新的上下文的函数。

这意味着 this 变量就是用户提供的变量。这在处理 JavaScript 事件时非常有用。

建议掌握这三个函数,以便用 JavaScript 编写功能代码。

3. 理解 JavaScript 作用域(以及闭包)

JavaScript 的作用域就像一个潘多拉宝盒。成百上千的面试难题都是由这一简单的概念演变而来。

作用域分为三种:

  • 全局作用域
  • 当前作用域/函数作用域
  • 块级作用域(ES6 中有介绍)

全局作用域是我们常用的:

代码语言:javascript
复制
x = 10;
function Foo() {
  console.log(x); // Prints 10
}
Foo()

当你在当前函数定义一个变量,函数作用域就出现了:

代码语言:javascript
复制
pi = 3.14;
function circumference(radius) {    
     pi = 3.14159;
     console.log(2 * pi * radius); // Prints "12.56636" not "12.56"
}
circumference(2);

ES16 标准引入了新的块级作用域,块级作用域将变量的作用范围限制在特定的括号内。

代码语言:javascript
复制
var a = 10;
function Foo() {
  if (true) {
    let a = 4;
  }
  alert(a); // alerts '10' because the 'let' keyword
}
Foo();

函数和判断条件都被视为块。在上面的例子中,条件判断为真故本该弹出4。但 ES6 破坏了块级变量的作用域,使之变成了全局作用域。

现在再来看看作用域的神奇之处。作用域可以通过闭包来实现。JavaScript 闭包就是一个函数返回另一个函数。

如果有人要求你:写一个传入字符串并返回单个字符的范例。一旦更新的字符串,输出也跟着替换掉旧的。这简称为生成器。

代码语言:javascript
复制
function generator(input) {
      var index = 0;
      return {
           next: function() {
                   if (index < input.length) {
                        index += 1;
                        return input[index - 1];
                   }
                   return "";
           } 
      }
}

生成器是这样执行的!

代码语言:javascript
复制
var mygenerator = generator("boomerang");
mygenerator.next(); // returns "b"
mygenerator.next() // returns "o"
mygenerator = generator("toon");
mygenerator.next(); // returns "t"

在这里,作用域扮演着重要角色。闭包是一个返回另一个函数和封装数据的函数。上面的字符生成器就是一个闭包。索引值在多个函数调用间保存。定义的内层函数可以访问外层函数定义的变量。这是不同的作用域。如果在二级函数里再定义一个函数,这个函数可以访问所有外层函数的变量。

针对 JavaScript 作用域可以问很多问题,吃透它吧。

4. 理解 this 关键词(全局,函数和对象范围)

用 JavaScript 编码,我们通常会用到函数和对象。如果是在浏览器上运行,全局上下文指的是 Window 对象。这意味着,打开浏览器的控制台并输入下面的内容,按下回车键,它会返回 true。

代码语言:javascript
复制
this === window;

当程序的上下文和作用域发生了改变,this 的指向也跟着改变。现在看看当前上下文的 this:

代码语言:javascript
复制
function Foo(){
  console.log(this.a);
}
var food = {a: "Magical this"};
Foo.call(food); // food is this

现在,预想下面输出。

代码语言:javascript
复制
function Foo(){
    console.log(this); // prints {}?
}

不会输出。因为在这是一个全局对象。记住,无论父级作用域是什么,子级都会继承父级作用域。因此它输出 Window 对象。以上讨论的三个方法实际是用来设置 this 对象的。

现在来看 this 的最后一种类型。对象作用域中的 this。如下:

代码语言:javascript
复制
var person = {
    name: "Stranger",
    age: 24,
    get identity() {
        return {who: this.name, howOld: this.age};
    }
}

这里用了 getter 语法,以参数形式去调用了一个函数。

代码语言:javascript
复制
person.identity; // returns {who: "Stranger", howOld: 24}

在这里,this 实际指向对象本身。正如我们之前提到的,this 在不同地方的表现不同。掌握 this 的用法吧。

5. 掌握对象的用法(Object.freeze, Object.seal)

很多人都知道这样的对象:

代码语言:javascript
复制
var marks = {physics: 98, maths:95, chemistry: 91};

它是保存键值对的映射。JavaScript 对象有一个特殊属性,可以将任何数据存储为值。这意味着我们可以以值的形式储存列表,另一个对象,函数等。诸如此类。

创建对象的方法有:

代码语言:javascript
复制
var marks = {};
var marks = new Object();

分别使用 JSON 对象的 stringify 和 parse 方法,可以轻松地将给定对象转换成 JSON 字符串和 JSON 对象。

代码语言:javascript
复制
// returns "{"physics":98,"maths":95,"chemistry":91}"
JSON.stringify(marks);
// Get object from string
JSON.parse('{"physics":98,"maths":95,"chemistry":91}');

那么关于对象我们需要知道什么?使用 Object.keys 遍历对象很容易

代码语言:javascript
复制
var highScore = 0;
for (i of Object.keys(marks)) {
   if (marks[i] > highScore)
      highScore = marks[i];
}

Object.value返回对象的值列表。

对象的其他重要函数包括:

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

Object.prototye 提供了包含许多应用的更重要的函数,其中一些是:

Object.prototye.hasOwnProperty 用来查找对象中是否存在指定的属性/键值。

代码语言:javascript
复制
marks.hasOwnProperty("physics"); // returns true
marks.hasOwnProperty("greek"); // returns false

Object.prototye.instanceof 评定给定的对象是否是特性原型的类型(将在下一部分介绍,它们属于函数)。

代码语言:javascript
复制
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 可以冻结对象,因此现有属性不会被修改。

代码语言:javascript
复制
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 不允许这么做。我们可以通过下面的方法查看给定的对象是否被冻结:

代码语言:javascript
复制
Object.isFrozen(finalizedMarks); // returns true

Object.seal 和 Object.freeze 略有不同。Object.seal 允许配置已有属性,但不允许添加新属性,不能增删已有属性。

代码语言:javascript
复制
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

我们也可以通过下面的方法检查给定的对象是否被密封:

代码语言:javascript
复制
Object.isSealed(marks); // returns true

6. 掌握原型继承

古典继承在 JavaScript 中被模拟。它是使用了原型方法。在 ES5,ES6 中看到的所有新的 class 语法都只是包裹在底层原型 OOP 的语法糖。使用 JavaScript 函数就能创建类。

代码语言: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 关键字)。我们可以给这些指定的类(函数)添加方法。添加类的方法可以是这样:

代码语言:javascript
复制
Animal.prototype.shout = function() {
    console.log(this.name + 'is ' + this.sound + 'ing...');
}

这里你可能会有疑问。类中并没有 sound 属性。对!这里根本没有定义 sound 属性。它是由继承父类的子类传递的。

在 JavaScript 中,继承是这样实现的:

代码语言:javascript
复制
function Dog(name, type) {
   Animal.call(this, name, type);
   this.sound = "bow";
}

定义一个更具体的函数 Dog。在这里,为了继承 Animal 类,我们需要引用 call 函数(上面讨论过)来传递 this 和其他参数。我们可以通过以下方法来实例化 German Shepard。

代码语言:javascript
复制
var pet = Dog("germanShepard", animalGroups.MAMMAL);
console.log(pet); // returns Dog {name: "germanShepard", type: 1, sound: "bow"}

我们并没有在子函数中声明 name 和 type, 而是调用了 Animal 函数并设置相应的属性。pet 从父类那里获得了属性(name, type)。那么方法也能继承吗?让我们一起来看看!

代码语言:javascript
复制
pet.shout(); // Throws error

什么?为什么会这样?出现这种情况是因为 JavaScript 不能继承父类的方法。如何解决这个问题呢?

代码语言:javascript
复制
// Link prototype chains
Dog.prototype = Object.create(Animal.prototype);
var pet = new Dog("germanShepard", animalGroups.MAMMAL);
// Now shout method is available
pet.shout(); // germanShepard is bowing...

像现在这样 shout 方法是可用的。我们可以通过 object.constructor 函数来检查 JavaScript 中指定对象的类。让我们看看 pet 的类是什么。

代码语言:javascript
复制
pet.constructor; // returns Animal

这个答案不够准确。Animal 是一个父类。但 pet 到底是什么类型?它属于 Dog 类型。这是因为 Dog 类的构造函数。

代码语言:javascript
复制
Dog.prototype.constructor; // returns Animal

返回 Animal。我们应该把它设置为 Dog 类本身,如此一来,类的所有实例(对象)会指向它从属于的正确类名。

代码语言:javascript
复制
Dog.prototype.constructor = Dog;

关于原型继承,请记住以下四点:

  • 类属性用this界定
  • 类方法使用prototype对象界定
  • 继承属性,请使用call函数传递this对象
  • 继承方法,请使用Object.create连接父类和子类的原型
  • 始终将子类构造函数设置为自身,以获取对象的正确标识

小注:即使在新的类语法中,也会在底层发生以上事件。知道这些对掌握JS知识很有帮助。

在 JS 中,call 函数和 prototype 对象造就了继承。

7. 理解回调函数和 promises

回调函数是在输入/输出操作完成后执行的。在 Python/Ruby 中,输入/输出的过程可能会阻塞代码而不允许进一步执行。但在 JavaScript 中,因其允许异步操作,所以可以给异步函数提供回调。例如,通过操作鼠标或键盘等,触发 AJAX(XMLHttpRequest)从浏览器调服务器接口。代码如下:

代码语言:javascript
复制
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 请求成功返回时,将执行该回调函数。

Promises 是回调函数的简洁封装器,能优雅的执行异步代码。本文讨论了很多关于 promises 的内容。这也是JS中应该掌握的一个重要内容。

8. 掌握正则表达式

正则表达式的用途很多。处理文本,限制用户的输入规则等。JavaScript 开发者应该掌握基本的正则表达式并用来解决实际问题。正则表达式是一个通用概念。接下来,一起来看看在 JS 中如何使用正则表达式。

我们可以通过以下方法创建一个新的正则表达式:

代码语言:javascript
复制
var re = /ar/;
var re = new RegExp('ar'); // This too works

上面的正则表达式表示与给定字符串匹配的表达式。一旦定义了一个正则表达式,我们可以尝试匹配和查看符合条件的字符串。我们可以使用exec函数来匹配字符串。

代码语言:javascript
复制
re.exec("car"); // returns ["ar", index: 1, input: "car"]
re.exec("cab"); // returns null

很少特殊的字符类可以用来构建复杂的正则表达式。

正则表达式包含许多类型的元素。其中一些是:

  • 字符:w-查找单词字符,d-查找数字,D-查找非数字字符
  • 字符类:[x-y]查找从x到y到字符,[^x]查找非x的任何字符
  • 量词:+,?,*(查找多个或0个匹配字符)
  • 边界:^(开头),$(结尾)

对以上内容加以举例说明,如下:

代码语言:javascript
复制
/* 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 = /dDw/;
re4.exec('1232W2sdf'); // returns ["2W2", index: 3, input: "1232W2sdf"]
re4.exec('W3q'); // returns null
/* Boundaries */
var re5 = /^dDw/;
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]+).*>(.*?)</>/;
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>"]

正则表达式的更多细节内容,请参考手册(http://www.rexegg.com/regex-quickstart.html)。

除了 exec 函数,还有 match, search 和 replace 函数,它们可通过正则表达式找到某个字符串。但这些函数应该应用于字符串本身。

代码语言:javascript
复制
"2345-678r9".match(/[a-z A-Z]/); // returns ["r", index: 8, input: "2345-678r9"]
"2345-678r9".replace(/[a-z A-Z]/, ""); // returns 2345-6789

开发者应掌握正则表达式这一重要内容,以便轻松解决复杂的问题。

9. 熟悉 Map, Reduce 和 Filter

函数式编程是当今的一个热门话题。许多编程语言都将诸如 lambdas 之类的函数概念添加到它们的新版本中(例如:Java 7以上版本)。JavaScript 对函数式编程的支持由来已久。我们需要深入学习三个主要函数。数学函数传进输入并返回输出。纯函数对于给定的的输入总是返回相同的输出。我们现在讨论的函数也满足纯度要求。

map

map 函数用在 JavaScript 数组中。map 函数通过将数组的每个元素传递给转换函数,并返回一个新数组。JS 数组中 map 的一般语法是:

代码语言:javascript
复制
arr.map((elem){
    process(elem)
    return processedValue
}) // returns new array with each element processed

假设,我们最近正在处理串行键中少量不需要的字符。我们需要把它们移走。我们不是通过循环和查找来移除字符,而是使用map达到相同的效果并获得结果数组。

代码语言:javascript
复制
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"]

小注:JavaScript ES6 使用箭头语法来定义函数。

map 携带一个函数参数。而该函数自身也带有参数。这个参数是从数组中筛选的。这个方法应用于数组中的所有元素,并返回处理过的元素。

reduce

ruduce 函数将指定的列表缩减为一个最终值。当然,通过循环数组并将结果保存在变量中也能实现相同的效果。但在这里,同样是将一个数组缩减成一个值,reduce 更为简洁。JS 中 reduce 的一般语法是:

代码语言:javascript
复制
arr.reduce((accumulator,
           currentValue,
           currentIndex) => {
           process(accumulator, currentValue)
           return intermediateValue/finalValue
}, initialAccumulatorValue) // returns reduced value

accumulator 保存中间值和最终值。currentIndex, currentValue 分别是当前数组元素的索引和值。initialAccumultorValue 是传递给函数的初始值。

reduce 的一个实际用途是合并数组中的数组元素。合并是将内部数组元素转换成一个简单数组。例如:

代码语言:javascript
复制
var arr = [[1, 2], [3, 4], [5, 6]];
var flattenedArray = [1, 2, 3, 4, 5, 6];

我们也可以通过常规迭代来实现这一点。但使用 reduce 一行代码就搞定了。神奇吧!

代码语言:javascript
复制
var flattenedArray = arr.reduce((accumulator, currentValue) => {
    return accumulator.concat(currentValue);
}, []); // returns [1, 2, 3, 4, 5, 6]

filter

这是第三种函数式编程概念。filter 与 map 用法相近,因为 filter 也是处理数组中的每个元素并最终返回另一个数组(而不像 reduce 返回一个值)。筛选后的数组长度可以小于或等于原始数组。因为相对于输出数组,传入的筛选条件不可能是极少/0。JS filter 的一般语法是:

代码语言:javascript
复制
arr.filter((elem) => {
   return true/false
})

这里的 elem 是数组的数据元素,而函数返回的 true/false 将表示包含/不包含被过滤元素。常见的例子是根据给定的开头和结尾条件筛选单词数组。假设要筛选一个以 t 开头且以 r 结尾的单词数组。

代码语言:javascript
复制
var words = ["tiger", "toast", "boat", "tumor", "track", "bridge"]
var newData = words.filter((elem) => {
   return elem.startsWith('t') && elem.endsWith('r') ? true:false;
}); // returns ["tiger", "tumor"]

当有人问及 JavaScript 函数式编程方面的问题,这三个函数应该能脱口而出。如你所见,这三种用法既保证了函数的纯度,又不改变原始数组。

10. 理解错误处理模式

这是许多开发者最不关心的 JavaScript 内容。屈指可数的开发者会讨论错误处理问题。一个好的开发方法就是,严谨的将 JS 代码封装在 try/catch 代码块中。

雅虎的 UI 工程师 Nicholas C.Zakas 早在 2008 年就说过“要时常假设代码出错,假设事件无法正常执行!并在服务器抛出报错信息。”

在 JavaScript 中,只要编码过程稍不留神,就可能出错。例如:

代码语言:javascript
复制
$("button").click(function(){
    $.ajax({url: "user.json", success: function(result){
        updateUI(result["posts"]);
    }});
});

在这里,我们掉进了默认结果总是 JSON 对象的陷阱。这样可能导致服务器崩溃并返回一个 null,而不是返回正确结果。在这种情况下,null 的[“posts”]将会抛出一个错误。正确的处理方法应该是这样!

代码语言:javascript
复制
$("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 将该信息打出到服务器。

代码语言:javascript
复制
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...");
}

这段代码主要做三件事:

  1. 监听window层级的错误
  2. 一旦出现问题就调用API
  3. 打出到服务器上!

执行代码前,可以使用新的布尔函数(ES5,ES6)检查变量是否有效,是否为 null 或 undefined。

代码语言:javascript
复制
if (Boolean(someVariable)) {
// use variable now
} else {
    throw new Error("Custom message")
}

时常考虑错误处理问题,不依赖浏览器而是依靠自己。可能会出错的!

11. 其他要点(提升, 事件冒泡)

对于 JavaScript 开发者来说,以上所有的概念都是基础知识。但了解少量的内部细节是非常有用的。比如了解 JavaScript 在浏览器中的工作机制。那什么是提升和事件冒泡呢?

提升

提升是在运行程序时将声明的变量提升到作用域的顶部的过程。

代码语言:javascript
复制
doSomething(foo); // used before
var foo; // declared later

将以上代码在像 Python 这样的脚本语言中运行时,它会抛出一个错误。变量需要先定义才能引用。即使 JS 是一种脚本语言,它也有提升机制。在这机制中,JavaScript VM 在运行程序时会做这两件事:

  1. 首先扫描程序,收集所有的变量和函数声明,并为其分配内存空间。
  2. 通过给指定的变量填充值来运行程序,如果没有指定值,则填充undefined。

在上面的代码片段中,控制台日志会输出“undefined”。这是因为先收集了变量 foo。VM 再给变量 foo 寻找有无与之对应的赋值。这种提升会导致许多JavaScript 场景,一些代码会在某些地方抛出错误,另一些则不知不觉引用了 undefined。你需要了解提升以消除这些模糊场景。

事件冒泡

现在来看看事件冒泡!根据高级软件工程师 Arun P 的说法:

“事件的冒泡和捕获是HTML DOM API事件传播的两种形式,当事件发生在一个元素内的另一个元素中,并且两个元素都执行了该事件。事件的传播模式决定元素接收事件的顺序。”

通过冒泡,事件首先在最内层元素捕获和处理,接着传播到外层元素。而捕获则相反。我们通常使用 addEventListener 函数来监听事件的执行。

代码语言:javascript
复制
addEventListener("click", handler, useCapture=false)

第三个参数 useCapture 是关键。它的默认值是 false。因此,这是一个冒泡模型,事件从最内层元素开始执行,然后向外传播直到到达父级元素。如果这个参数为 true,那么它就是捕获模型。

例如:冒泡模型

代码语言:javascript
复制
<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 元素,程序的执行顺序与冒泡模型(默认情况)类似。

代码语言:javascript
复制
handler() => ulHandler() => divHandler()

如上图所示,程序按顺序依次向外触发。类似地,捕获模型则按顺序依次向内触发,即从父元素向内直到被点击的元素。现在修改上面代码中的这一行。

代码语言:javascript
复制
document.getElementById("foo").addEventListener("click", handler, true)

程序的执行顺序将是:

代码语言:javascript
复制
divHandler => ulHandler() => handler()

我们应该正确理解事件冒泡(触发方向是向内还是向外),这有助于实现用户界面(UI)而避免任何不必要的行为。

以上就是 JavaScript 的基本概念。正如我一开始说的,除了掌握这些概念,工作经验、知识和充分的准备都有助于破解 JavaScript 面试。请做好终身学习的准备。留意最新的技术进展(ES6)。深入了解 JavaScript 的方方面面,比如 V6 引擎,测试等。这里有一些视频资源供大家学习。最后,如果没有掌握数据结构和算法,任何面试都是不会成功的。Oleksii Trekhleb 策划了一个非常棒的 git repo 项目,他用 JS 编写了关于面试准备的算法。了解下吧(https://github.com/trekhleb/javascript-algorithms)。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端自习课 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
数据保险箱
数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档