脑图学习 JavaScript 之犀牛书【四 · 二】表达式、计算顺序

介绍

本篇讲解第四章讲到的关于 表达式 的定义、复杂表达式的计算过程express

自我提问

  • new 操做符作了什么?
    function Class() {
        this.a = 1;
        return {a: 2};
    }
    new Class();
    复制代码
  • 复杂表达式的计算顺序是怎么判定的?
    var a = {i: 1};
    var b = a;
    a.j = a = {k: 2};
    复制代码

脑图

关键知识点

表达式的定义

犀牛书中对 表达式 的定义为 能够计算出结果的短语,简单来讲就是 有返回值的代码数组

MDN 上对表达式的定义是:bash

An expression is any valid unit of code that resolves to a value.函数

表达式是一组代码的集合,它返回一个值。post

按照这些定义理解,只要有返回值的代码均可以认为是表达式。学习

然而不肯定理解是否有误,毕竟就好像函数声明,也有返回值,为啥就和函数表达式区分开,查了好久也没找到确切的定义。猜想定义应该是有操做符参与计算的才算表达式,毕竟函数表达式和声明写法上差的就是赋值,并且表达式和运算符通常都是一块儿出现的。ui

表达式类型

看下常见的表达式类型this

原始表达式

原始表达式是表达式的 最小单位,常量、直接量、关键字、变量spa

null
undefined
true
false
this
someVar
1.2
'string'
复制代码

犀牛书这里有注明,null 是关键字,可是 undefined 是全局变量。确实是这样,能够试试给 null 和 undefined 赋值,但为啥这样设计呢,不懂。 prototype

对象、数组初始化表达式

[]
[1, 2, 1+3]
{a: 1, b: 2}
复制代码

函数定义表达式

var func = function(x) {
    return x * x;
}
复制代码

属性访问表达式

a.x
a[x]
复制代码

这里犀牛书有说了很重要的一点:无论使用哪一种形式的属性访问表达式,在 . 和 [ 以前的表达式老是会首先计算。本文最下方有关于这个的验证。

调用表达式

fun(x)
Math.max(1, 2)
复制代码

调用表达式前的表达式是一个属性访问表达式时这个调用叫作 方法调用,方法调用会将方法中的 this 指向调用的对象。

非方法调用表达式非严格模式会使用全局对象做为 this 关键字的值,严格模式下为 undefined。

对象建立表达式

new Object()
new String
复制代码

对象建立表达式的建立步骤:

  1. 建立一个新的空对象
  2. 将构造函数 prototype 挂到空对象的原型链上(这个犀牛书上没说,由于这里书上说的比较粗,具体的会在第 9 章讲解)
  3. 将这个新对象做为构造函数中 this 关键字的值
  4. 经过执行构造函数初始化新对象的属性
  5. 若是构造函数返回了一个值,新对象会被废弃,返回值会做为表达式的值,不然会将这个新对象做为表达式的值

算数表达式

1 + 2
's' + 'a'
1 - null
++a
12 | 1
12 ^ 4
~12
12 << 1
12 >> 1
-12 >>> 1
复制代码

关系表达式

null == undefined
a = 1
a === b
a != 1
a !== 1
'a' < 'b'
'a' > 'b'
'a' <= 'b'
'a' >= 'b'
'x' in y
a instanceof Date
复制代码

逻辑表达式

x == 0 && y == 0
x == 0 || y == 0
!x
复制代码

赋值表达式

x = 1
a.b = 1
a++
b--
--a
++b
a += b
b *= a
复制代码

eval 表达式

eval('1 + 2')
复制代码

其它表达式

a ? 1 : 2
typeof true
delete a.b
void a++
a++, b++
复制代码

复杂表达式

将简单表达式链接在一块儿就能够构成复杂表达式。

var a = b = c + 1 / (1 + 2) ? 1 : 2 * 100 - typeof '123'
复制代码

运算顺序图解

这里结合上一篇的运算符的相关知识对一些复杂表达式进行解析,看看复杂表达式究竟是如何计算的。主要关系运算符的优先级、结合性、左值等概念,不太了解的能够看下上一篇

先来个网红题目简单解析一下

var a = {i: 1};
var b = a;
a.j = a = {k: 2};
复制代码

  1. 取出 a.j 的左值
  2. 取出 a 的左值
  3. 将 {k: 2} 赋值给 a
  4. 将 a = {k: 2} 的返回值赋值给 a.j

为何要先取出 a.j 的内存地址:

  1. 上方属性访问表达式有说过,无论使用哪一种形式的属性访问表达式,在 . 和 [ 以前的表达式老是会首先计算
  2. . 的运算符优先级高于 =,因此会优先计算,在赋值语句执行前就会执行 . 运算符取出 a.j 的左值

验证一下第一个说法是否正确:

var b = {i: 1};
var a = {i: 1, a: b};
a.a = 2, a.a.i = 3;
复制代码

按照犀牛书的说法 a.a.i 中的 a.a 会优先计算为 b,而后 b 的 i 被赋值为 3,不过经验证这个说法并不正确。 因此其实并无这个额外的规则,只是单纯的运算符优先级和左值的问题。

再来个复杂一点的例子,看下表达式计算时如何推断优先级。

var k = {
    get a() {
        console.log('a');
        return 'a';
    },
    get b() {
        console.log('b');
        return 'b';
    },
    get c() {
        console.log('c');
        return 'c';
    }
};
var b = { b: 1 };
var a = { a: 1, b: b, c: 2 };
var c = { c: 1 };
var v = 2;
a[k.a] = b[k.b] = (v * 10 + c[k.c] / 10 + v++ - ++v) | 1;
// a
// b
// c
// { a: 19, b: { b: 19 }, c: 2 } { b: 19 } 4
console.log(a, b, v);
复制代码

按照运算符的 优先级从低到高 分解成最基础的表达式和运算符,按照 结合性结合相同优先级 的运算符,而后按照 从左往右 的顺序计算子表达式,深度优先 计算表达式。

因此顺序是

a[k.a]
b[k.b]
v * 10
c[k.c]
(c[k.c]) / 10
v++
++v
(v * 10) + ((c[k.c]) / 10) + (v++) - (++v)
((v * 10) + ((c[k.c]) / 10) + (v++) - (++v)) | 1
b[k.b] = (((v * 10) + ((c[k.c]) / 10) + (v++) - (++v)) | 1)
a[k.a] = (b[k.b] = (((v * 10) + ((c[k.c]) / 10) + (v++) - (++v)) | 1))
复制代码

按照上述过程将复杂表达式拆解后,计算过程就一目了然了。

系列文章目录

  1. 脑图学习 JavaScript 之犀牛书【一】
  2. 脑图学习 JavaScript 之犀牛书【二】词法结构
  3. 脑图学习 JavaScript 之犀牛书【三 · 一】数据类型
  4. 脑图学习 JavaScript 之犀牛书【三 · 二】类型转换、变量
  5. 脑图学习 JavaScript 之犀牛书【四 · 一】运算符、类型转换
  6. 脑图学习 JavaScript 之犀牛书【四 · 二】表达式
相关文章
相关标签/搜索