前端技术Node.js专题前端知识

你所不知道的 JavaScript II

2019-02-09  本文已影响93人  一俢

初识 JavaScript

JavaScript 出生于 1995 年,目前它服务在 92% 左右的网站上面,GitHub 开源社区中 20% 左右的项目采用了 JavaScript。在互联网兴起的年代 JavaScript 发展迅猛,以 IE 为首的微软公司开始制定起自己的 JavaScript 标准,包括火狐、Opera 等,这时候标准泛滥,开发人员为了兼容各个版本浏览器每天加班加点,代码也越来越难以维护。后来 ECMA(欧洲计算机制造联合会,一个统一电脑操作格式标准、程序语言和输入输出的组织)创建了 ECMAScript(ES)标准,现在越来越多的浏览器开始遵从这一标准,此时的 WEB 前端语言才开始真正的统一起来,现在浏览器大部分都兼容 ES5(第五个版本)标准,ES6 在 2015 年已经发布,现在很多工程师已经开始使用新的标准开发,相信 JavaScript 未来会越来越完善越来越流行。

JavaScript 这种弱类型的语言,入门很简单,但是想玩转它需要下一些功夫,因为它和常规语言的语法有很多不同之处,通常不安常理出牌,如果不摸清楚它的性格,可能在出现 bug 的时候会困扰你半天。我从众多 JavaScript 反常规思维的语法中挑选了一些例子,知道了这些后,会帮助你更好的解决实践过程中的问题。

JavaScript 和 Java 的区别

很多人会将两者混为一谈,其实它们没有血缘关系!

javascript-vs-java.png

JavaScript 和 Java 的关系好比仓鼠和火腿的关系

混沌中的全局变量和局部变量

写给非工程师的备注:变量的声明好比在医院挂号,声明不同类型的变量,就好比你挂不同类型的号。那么作用域就好比医院,不可能你在民航医院挂的号能在朝阳医院也可以用

var a = 1;
b = 2;

function fn1() {
    var c = a;
    d = 4;
}

function fn2() {
    console.log(d);
}

fn1();
fn2();

运行这段代码,居然会打印出 4,后端 Java 工程师志兵吓一大跳,不可理喻,并且会对 JavaScript 语言的设计者破口大骂:“啥xx语言!”。常见的计算机语言中,变量是有一定的作用域的,在作用域内部的变量只会在这个作用域内部有效果,超出它的作用域后就无法使用了。

但是奇葩的 JavaScript 就是和别人不一样,d 这个变量仿佛穿越了时空,原来在 JavaScript 中,变量的声明有如下特征:

// begin: 变量的声明提升
var a;
window.b;
window.d;
// end: 变量的声明提升

a = 1;
window.b = 2;

function fn1() {
    // begin: 变量的声明提升
    var c;
    // end: 变量的声明提升

    c = 3;
    window.d = 4;
}

function fn2() {
    console.log(window.d);
}

fn1();
fn2();

课后作业:先有鸡还是先有蛋?

有了变量声明会被提升的知识后,看看下面两段代码,它们运行的结果如何?

a = 2;
var a;
console.log(a);
console.log(a);
var a = 2;

生死冤家 nullundefined

nullundefined 到底什么区别,经常搞得我们非常苦恼,判断一个变量存不存在我们该选谁?

var a = null;
var b;

console.log(a);
console.log(b);

console.log(typeof a);
console.log(typeof b);

console.log(a == b);
console.log(a === b);

原来,这与 JavaScript 的历史有关。1995 年 JavaScript 诞生时,最初像Java一样,只设置了 null 作为表示"无"的值。根据 C 语言的传统,null 被设计成可以自动转为 0。但是 JavaScript 的设计者 Brendan Eich,觉得这样做还不够,于是又设计了一个 undefined

课后作业:nullundefined0 的比较?

我们知道 (null == undefined) => true 它们两者值是相等的,那么:

console.log(null == 0);
console.log(undefined == 0);

1 + - + - - + + + - + 1 什么鬼?

看到如此神奇的运算,想必小伙伴们都惊呆了,是的!你看到的这一串在 JavaScrpit 中是可以运行起来的。运算还可以如此玩?

如何计算:

1 + - + - - + + + - + 1
1 +(-(+(-(-(+(+(+(-(+(1))))))))))

总结规律:

琢磨不透的数字之间的计算

var a = 0.2 - 0.1; // => 0.1
var b = 0.3 - 0.2; // => 0.09999999999999998

怎么会这样!我喝醉了吗?刚刚上线完的长顺不敢相信自己的眼睛,抓耳挠腮,这可咋办?又要上一次线了!

这是由于精度造成的,JavaScript 的数字(无论是整数还是小数)采用的是64位双精度浮点类型,存储方式如下:

1位符号位+11位指数位+52位小数位

当在运算过程中超过了存储的位数,将会丢失因为丢失数据产生精度问题。

0.1 => 0.0001 1001 1001 1001…(无限循环)
0.2 => 0.0011 0011 0011 0011…(无限循环)

NaN What the xxxx is this?

NaN 性格非常纠结,它是数字类型,但是又代表不是一个数字(Not a Number),当在做计算时产生错误通常会得到 NaN 这个结果;这位哥们性格也非常孤僻,孤僻到和自己都合不来。

console.log(typeof NaN); // => number
console.log(NaN == 1);   // => false
console.log(NaN > 1);    // => false
console.log(NaN < 1);    // => false
console.log(NaN == NaN); // => false
console.log(NaN > NaN);  // => false
console.log(NaN < NaN);  // => false

课后作业:NaN != NaN ?

1/00/0这也行?

在很多语言中 0 是不能被除的,甚至都无法通过编译,但是神奇的 JavaScript 不会报错,并且会给你一个满意的结果:

变幻莫测的 this

在很多语言中都有 this 这个关键词,它提供了一种优雅的方式来隐式传递一个对象引用,在很多面向对象的语言中(Java、C#),this 关键字的含义非常明确,就是代表当前对象,通常在编译期间就确定下来(编译期绑定);但是在 JavaScript 的世界中 this 是动态绑定的会让你摸不着头脑,经常会发现 this 并不是自己想要的这个

我们把 this 的指向分成以下几种情况:

外部函数调用

外部函数中调用 this 时,它将指向全局对象,浏览器中即为 window

function whatIsThis() {
    console.log(this);
}
whatIsThis();

会打印出 window 对象

内部函数调用

内部函数中调用this时,它和外部函数调用一样,指向了全局对象:

var obj = {
    a: 0,
    fn: function(){
        var whatIsThis = function(){
            console.log(this);
        }
        
        whatIsThis();
    }
};
obj.fn();

会打印出Window对象

对象的方法调用

对象的方法中调用this时,它将指向这个对象:

var obj = {
    a: 0,
    whatIsThis:() => {
        console.log(this);
    }
};
obj.whatIsThis();

会打印出当前Object对象

构造函数内部调用

构造函数的对象调用this时,它将指向这个对象:

function Class() {
    console.log(this);
}
var obj = new Class();

这里总结一下 this

this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用:用对象调用的方法中使用的 this 会指向该对象;直接调用的函数中使用的 this 会指向全局对象 window

课后作业:this 是否能给它赋上另外一个对象呢?

在 JavaScript 中很多对象都可以进行扩展,包括它自带的内置对象,那么 this 可以吗?

邪恶(evil)的 eval

eval 很强大,它可以将字符串当作 JavaScript 运行,也就是说它能让代码在代码中运行;当然控制不好对于程序来说也是一个灾难,它会把你的作用域弄的非常混乱,很多 JavaScript 的编辑器的插件不允许使用 eval 这个关键字。

var b = 2;

function foo(str, a) {
    eval(str);
    console.log(a,b);
}

foo('var b = 3;', 1); //=>1, 3
function foo(str, a) {
    var b = 3;
    console.log(a,b);
}

阅读上面的代码,我们将字符串输入到 foo 方法中,最后导致了意想不到的结果,输入的内容动态的变成了代码的一部分,使得变量 b 在函数中的作用域产生了变化,并且屏蔽了外部作用域中的同名变量。这样会导致程序原本的逻辑产生变化,它就好像一个不讲道理的小孩,从不按套路出牌,让我们的程序难以控制,我们不建议使用它。最后,在程序中动态生成代码的使用场景非常罕见,因为它所带来的好处无法抵消性能上的损失。

with or without

with 这个语法也是一个比较难掌握的,和 eval 一样它会让程序的作用域非常混乱。它本身的作用是:当重复引用同一个对象中的多个属性时,可以使用这个快捷方式不需要重复引用对象本身。

var obj = {
    a: 1,
    b: 2,
    c: 3
};

// 重复的使用 obj
obj.a = 2;
obj.b = 3;
obj.c = 4;

// 快捷的使用 obj
with(obj) {
    a = 3;
    b = 4;
    c = 5;
}

看起来这种方式的确很好用,我们不用重复的去调用某个对象而对其中的属性一个一个赋值了,让开发更佳简便了。但是,糟糕的事情总是会在 JavaScript 中发生:

function foo(obj) {
    with(obj) {
        a = 2;
    }
}

var o1 = {
    a: 3
};

var o2 = {
    b: 3
};

foo(o1);
console.log(o1.a); // 2

foo(o2);
console.log(o2.a); // undefined
console.log(a);    // 2 —— ops! a 被泄露到全局作用域上了

结论:with 不能使用在一个该对象不存在的属性上,否则会将该属性变成一个全局对象!这种改变作用域的事情是一件非常恐怖的事情,不建议在实际项目中使用它;

可计算属性名

我们知道很多面相对象的语言中,可以通过 . 获取到某个对象下的某个属性,例如:person.name。当然这一点在 JavaScript 中完全没有问题,更佳恐怖的是属性名可以是变量,还可以传入表达式,我们叫做可计算属性名:

var person = {
    name: 'jay'
};

var propertyName = 'name';

console.log(person.name);
console.log(person['name']);
console.log(person[propertyName]);
console.log(person['na' + 'me']); // ES6

火眼金星看清一切的 for/in

for/in 这个语句可以遍历对象的属性,它就好像 JavaC# 中的反射机制一样,能够通过它窥探某个类型的基本结构,我们可以采用层层递归的方式,看清楚某个对象的所有属性。

function foo(obj) {
    for(key in obj) {
        console.log(obj[key]);
    }
}

将对象打回 原型

原型在 JavaScript 中是一个非常重要也非常难理解的知识点,搞清楚它可能需要一个专门的话题来讲它,现在我们只在这里简单的讨论。

prototype__proto__ 的关系相当复杂,prototype 为构造函数原型,__proto__ 为对象的原型。所有构造函数的 __proto__ 都指向 Function.prototype;Function.prototype 的原型指向 Object.prototype,而 Object.prototype 的原型为空 null,这样就构成了一条复杂的原型链。

var klass = function() {
    this.name = "jay";
}
var obj = new klass();

console.log(obj.__proto__ === klass.prototype);
console.log(klass.prototype.__proto__ === Object.prototype);
console.log(Object.prototype.__proto__ === null);

console.log(klass.constructor.__proto__ === Function.prototype);
console.log(Function.prototype.__proto__ === Object.prototype);
console.log(Object.prototype.__proto__ === null);

//> Console:
//> true
//> true
//> true
//> true
//> true
//> true
javascript-prototype.png
Object.__proto__ === Function.__proto__
Function.__proto__ === Function.prototype

最后

工程师的工作非常枯燥,不过我们可以在枯燥的工作中找到一些有趣的事情。希望我们的工程师们能够善于发现工作中的乐趣,这样就会越来越充满激情。

上一篇下一篇

猜你喜欢

热点阅读