勤与奋
专注国外LEAD联盟

07-JavaScript对象:我们真的需要模拟类吗?

勤与奋阅读(131)

早期的JavaScript程序员一般都有过使用JavaScript“模拟面向对象”的经历。

在上一篇文章我们已经讲到,JavaScript本身就是面向对象的,它并不需要模拟,只是它实现面向对象的方式和主流的流派不太一样,所以才让很多人产生了误会。

那么,随着我们理解的思路继续深入,这些“模拟面向对象”,实际上做的事情就是“模拟基于类的面向对象”。

尽管我认为,“类”并非面向对象的全部,但我们不应该责备社区出现这样的方案,事实上,因为一些公司的政治原因,JavaScript推出之时,管理层就要求它去模仿Java。

所以,JavaScript创始人Brendan Eich在“原型运行时”的基础上引入了new、this等语言特性,使之“看起来语法更像Java”,而Java正是基于类的面向对象的代表语言之一。

但是JavaScript这样的半吊子模拟,缺少了继承等关键特性,导致大家试图对它进行修补,进而产生了种种互不相容的解决方案。

庆幸的是,从ES6开始,JavaScript提供了class关键字来定义类,尽管,这样的方案仍然是基于原型运行时系统的模拟,但是它修正了之前的一些常见的“坑”,统一了社区的方案,这对语言的发展有着非常大的好处。

实际上,我认为“基于类”并非面向对象的唯一形态,如果我们把视线从“类”移开,Brendan当年选择的原型系统,就是一个非常优秀的抽象对象的形式。

我们从头讲起。

什么是原型?

原型是顺应人类自然思维的产物。中文中有个成语叫做“照猫画虎”,这里的猫看起来就是虎的原型,所以,由此我们可以看出,用原型来描述对象的方法可以说是古已有之。

我们在上一节讲解面向对象的时候提到了:在不同的编程语言中,设计者也利用各种不同的语言特性来抽象描述对象。

最为成功的流派是使用“类”的方式来描述对象,这诞生了诸如 C++、Java等流行的编程语言。这个流派叫做基于类的编程语言。

还有一种就是基于原型的编程语言,它们利用原型来描述对象。我们的JavaScript就是其中代表。

“基于类”的编程提倡使用一个关注分类和类之间关系开发模型。在这类语言中,总是先有类,再从类去实例化一个对象。类与类之间又可能会形成继承、组合等关系。类又往往与语言的类型系统整合,形成一定编译时的能力。

与此相对,“基于原型”的编程看起来更为提倡程序员去关注一系列对象实例的行为,而后才去关心如何将这些对象,划分到最近的使用方式相似的原型对象,而不是将它们分成类。

基于原型的面向对象系统通过“复制”的方式来创建新对象。一些语言的实现中,还允许复制一个空对象。这实际上就是创建一个全新的对象。

基于原型和基于类都能够满足基本的复用和抽象需求,但是适用的场景不太相同。

这就像专业人士可能喜欢在看到老虎的时候,喜欢用猫科豹属豹亚种来描述它,但是对一些不那么正式的场合,“大猫”可能更为接近直观的感受一些(插播一个冷知识:比起老虎来,美洲狮在历史上相当长时间都被划分为猫科猫属,所以性格也跟猫更相似,比较亲人)。

我们的JavaScript 并非第一个使用原型的语言,在它之前,self、kevo等语言已经开始使用原型来描述对象了。

事实上,Brendan更是曾透露过,他最初的构想是一个拥有基于原型的面向对象能力的scheme语言(但是函数式的部分是另外的故事,这篇文章里,我暂时不做详细讲述)。

在JavaScript之前,原型系统就更多与高动态性语言配合,并且多数基于原型的语言提倡运行时的原型修改,我想,这应该是Brendan选择原型系统很重要的理由。

原型系统的“复制操作”有两种实现思路:

  • 一个是并不真的去复制一个原型对象,而是使得新对象持有一个原型的引用;
  • 另一个是切实地复制对象,从此两个对象再无关联。

历史上的基于原型语言因此产生了两个流派,显然,JavaScript显然选择了前一种方式。

JavaScript的原型

如果我们抛开JavaScript用于模拟Java类的复杂语法设施(如new、Function Object、函数的prototype属性等),原型系统可以说相当简单,我可以用两条概括:

  • 如果所有对象都有私有字段[[prototype]],就是对象的原型;
  • 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。

这个模型在ES的各个历史版本中并没有很大改变,但从 ES6 以来,JavaScript提供了一系列内置函数,以便更为直接地访问操纵原型。三个方法分别为:

  • Object.create 根据指定的原型创建新对象,原型可以是null;
  • Object.getPrototypeOf 获得一个对象的原型;
  • Object.setPrototypeOf 设置一个对象的原型。

利用这三个方法,我们可以完全抛开类的思维,利用原型来实现抽象和复用。我用下面的代码展示了用原型来抽象猫和虎的例子。

var cat = {
    say(){
        console.log("meow~");
    },
    jump(){
        console.log("jump");
    }
}

var tiger = Object.create(cat,  {
    say:{
        writable:true,
        configurable:true,
        enumerable:true,
        value:function(){
            console.log("roar!");
        }
    }
})


var anotherCat = Object.create(cat);

anotherCat.say();

var anotherTiger = Object.create(tiger);

anotherTiger.say();

这段代码创建了一个“猫”对象,又根据猫做了一些修改创建了虎,之后我们完全可以用Object.create来创建另外的猫和虎对象,我们可以通过“原始猫对象”和“原始虎对象”来控制所有猫和虎的行为。

但是,在更早的版本中,程序员只能通过Java风格的类接口来操纵原型运行时,可以说非常别扭。

考虑到new和prototype属性等基础设施今天仍然有效,而且被很多代码使用,学习这些知识也有助于我们理解运行时的原型工作原理,下面我们试着回到过去,追溯一下早年的JavaScript中的原型和类。

早期版本中的类与原型

在早期版本的JavaScript中,“类”的定义是一个私有属性 [[class]],语言标准为内置类型诸如Number、String、Date等指定了[[class]]属性,以表示它们的类。语言使用者唯一可以访问[[class]]属性的方式是Object.prototype.toString。

以下代码展示了所有具有内置class属性的对象:

    var o = new Object;
    var n = new Number;
    var s = new String;
    var b = new Boolean;
    var d = new Date;
    var arg = function(){ return arguments }();
    var r = new RegExp;
    var f = new Function;
    var arr = new Array;
    var e = new Error;
    console.log([o, n, s, b, d, arg, r, f, arr, e].map(v => Object.prototype.toString.call(v))); 

因此,在ES3和之前的版本,JS中类的概念是相当弱的,它仅仅是运行时的一个字符串属性。

在ES5开始,[[class]] 私有属性被 Symbol.toStringTag 代替,Object.prototype.toString 的意义从命名上不再跟 class 相关。我们甚至可以自定义 Object.prototype.toString 的行为,以下代码展示了使用Symbol.toStringTag来自定义 Object.prototype.toString 的行为:

    var o = { [Symbol.toStringTag]: "MyObject" }
    console.log(o + "");

这里创建了一个新对象,并且给它唯一的一个属性 Symbol.toStringTag,我们用字符串加法触发了Object.prototype.toString的调用,发现这个属性最终对Object.prototype.toString 的结果产生了影响。

但是,考虑到JavaScript语法中跟Java相似的部分,我们对类的讨论不能用“new运算是针对构造器对象,而不是类”来试图回避。

所以,我们仍然要把new理解成JavaScript面向对象的一部分,下面我就来讲一下new操作具体做了哪些事情。

new 运算接受一个构造器和一组调用参数,实际上做了几件事:

  • 以构造器的 prototype 属性(注意与私有字段[[prototype]]的区分)为原型,创建新对象;
  • 将 this 和调用参数传给构造器,执行;
  • 如果构造器返回的是对象,则返回,否则返回第一步创建的对象。

new 这样的行为,试图让函数对象在语法上跟类变得相似,但是,它客观上提供了两种方式,一是在构造器中添加属性,二是在构造器的 prototype 属性上添加属性。

下面代码展示了用构造器模拟类的两种方法:


function c1(){
    this.p1 = 1;
    this.p2 = function(){
        console.log(this.p1);
    }
} 
var o1 = new c1;
o1.p2();



function c2(){
}
c2.prototype.p1 = 1;
c2.prototype.p2 = function(){
    console.log(this.p1);
}

var o2 = new c2;
o2.p2();

第一种方法是直接在构造器中修改this,给this添加属性。

第二种方法是修改构造器的prototype属性指向的对象,它是从这个构造器构造出来的所有对象的原型。

没有Object.create、Object.setPrototypeOf 的早期版本中,new 运算是唯一一个可以指定[[prototype]]的方法(当时的mozilla提供了私有属性__proto__,但是多数环境并不支持),所以,当时已经有人试图用它来代替后来的 Object.create,我们甚至可以用它来实现一个Object.create的不完整的polyfill,见以下代码:

Object.create = function(prototype){
    var cls = function(){}
    cls.prototype = prototype;
    return new cls;
}

这段代码创建了一个空函数作为类,并把传入的原型挂在了它的prototype,最后创建了一个它的实例,根据new的行为,这将产生一个以传入的第一个参数为原型的对象。

这个函数无法做到与原生的Object.create一致,一个是不支持第二个参数,另一个是不支持null作为原型,所以放到今天意义已经不大了。

ES6 中的类

好在ES6中加入了新特性class,new跟function搭配的怪异行为终于可以退休了(虽然运行时没有改变),在任何场景,我都推荐使用ES6的语法来定义类,而令function回归原本的函数语义。下面我们就来看一下ES6中的类。

ES6中引入了class关键字,并且在标准中删除了所有[[class]]相关的私有属性描述,类的概念正式从属性升级成语言的基础设施,从此,基于类的编程方式成为了JavaScript的官方编程范式。

我们先看下类的基本写法:

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
  // Getter
  get area() {
    return this.calcArea();
  }
  // Method
  calcArea() {
    return this.height * this.width;
  }
}

在现有的类语法中,getter/setter和method是兼容性最好的。

我们通过get/set关键字来创建getter,通过括号和大括号来创建方法,数据型成员最好写在构造器里面。

类的写法实际上也是由原型运行时来承载的,逻辑上JavaScript认为每个类是有共同原型的一组对象,类中定义的方法和属性则会被写在原型对象之上。

此外,最重要的是,类提供了继承能力。我们来看一下下面的代码。

class Animal { 
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(this.name + ' makes a noise.');
  }
}

class Dog extends Animal {
  constructor(name) {
    super(name); // call the super class constructor and pass in the name parameter
  }

  speak() {
    console.log(this.name + ' barks.');
  }
}

let d = new Dog('Mitzie');
d.speak(); // Mitzie barks.

以上代码创造了Animal类,并且通过extends关键字让Dog继承了它,展示了最终调用子类的speak方法获取了父类的name。

比起早期的原型模拟方式,使用extends关键字自动设置了constructor,并且会自动调用父类的构造函数,这是一种更少坑的设计。

所以当我们使用类的思想来设计代码时,应该尽量使用class来声明类,而不是用旧语法,拿函数来模拟对象。

一些激进的观点认为,class关键字和箭头运算符可以完全替代旧的function关键字,它更明确地区分了定义函数和定义类两种意图,我认为这是有一定道理的。

总结

在新的ES版本中,我们不再需要模拟类了:我们有了光明正大的新语法。而原型体系同时作为一种编程范式和运行时机制存在。

我们可以自由选择原型或者类作为代码的抽象风格,但是无论我们选择哪种,理解运行时的原型系统都是很有必要的一件事。

在你的工作中,是使用class还是仍然在用function来定义“类”?为什么这么做?如何把使用function定义类的代码改造到class的新语法?

欢迎给我留言,我们一起讨论。

05-JavaScript类型:关于类型,有哪些你不知道的细节?

勤与奋阅读(114)

你好,我是winter。今天我们来讲讲JavaScript的内容,在这个部分,我首先想跟你聊一聊类型。

JavaScript类型对每个前端程序员来说,几乎都是最为熟悉的概念了。但是你真的很了解它们吗?我们不妨来看看下面的几个问题。

  • 为什么有的编程规范要求用void 0代替undefined?
  • 字符串有最大长度吗?
  • 0.1 + 0.2不是等于0.3么?为什么JavaScript里不是这样的?
  • ES6新加入的Symbol是个什么东西?
  • 为什么给对象添加的方法能用在基本类型上?

如果你答起来还有些犹豫的地方,这就说明你对这部分知识点,还是有些遗漏之处的。没关系,今天我来帮你一一补上。

我在前面提到过,我们的JavaScript模块会从运行时、文法和执行过程三个角度去剖析JS的知识体系,本篇我们就从运行时的角度去看JavaScript的类型系统。

运行时类型是代码实际执行过程中我们用到的类型。所有的类型数据都会属于7个类型之一。从变量、参数、返回值到表达式中间结果,任何JavaScript代码运行过程中产生的数据,都具有运行时类型

类型

JavaScript语言的每一个值都属于某一种数据类型。JavaScript语言规定了7种语言类型。语言类型广泛用于变量、函数参数、表达式、函数返回值等场合。根据最新的语言标准,这7种语言类型是:

  1. Undefined;
  2. Null;
  3. Boolean;
  4. String;
  5. Number;
  6. Symbol;
  7. Object。

除了ES6中新加入的Symbol类型,剩下6种类型都是我们日常开发中的老朋友了,但是,要想回答文章一开始的问题,我们需要重新认识一下这些老朋友,下面我们就来从简单到复杂,重新学习一下这些类型。

Undefined、Null

我们的第一个问题,为什么有的编程规范要求用void 0代替undefined?现在我们就分别来看一下。

Undefined 类型表示未定义,它的类型只有一个值,就是 undefined。任何变量在赋值前是 Undefined 类型、值为 undefined,一般我们可以用全局变量undefined(就是名为undefined的这个变量)来表达这个值,或者 void 运算来把任一一个表达式变成 undefined 值。

但是呢,因为JavaScript的代码undefined是一个变量,而并非是一个关键字,这是JavaScript语言公认的设计失误之一,所以,我们为了避免无意中被篡改,我建议使用 void 0 来获取undefined值。

Undefined跟 null 有一定的表意差别,null表示的是:“定义了但是为空”。所以,在实际编程时,我们一般不会把变量赋值为 undefined,这样可以保证所有值为 undefined 的变量,都是从未赋值的自然状态。

Null 类型也只有一个值,就是 null,它的语义表示空值,与 undefined 不同,null 是 JavaScript 关键字,所以在任何代码中,你都可以放心用 null 关键字来获取 null 值。

Boolean

Boolean 类型有两个值, true 和 false,它用于表示逻辑意义上的真和假,同样有关键字 true 和 false 来表示两个值。这个类型很简单,我就不做过多介绍了。

String

我们来看看字符串是否有最大长度。

String 用于表示文本数据。String 有最大长度是 2^53 – 1,这在一般开发中都是够用的,但是有趣的是,这个所谓最大长度,并不完全是你理解中的字符数。

因为String 的意义并非“字符串”,而是字符串的 UTF16 编码,我们字符串的操作 charAt、charCodeAt、length 等方法针对的都是 UTF16 编码。所以,字符串的最大长度,实际上是受字符串的编码长度影响的。

Note:现行的字符集国际标准,字符是以 Unicode 的方式表示的,每一个 Unicode 的码点表示一个字符,理论上,Unicode 的范围是无限的。UTF是Unicode的编码方式,规定了码点在计算机中的表示方法,常见的有 UTF16 和 UTF8。 Unicode 的码点通常用 U+??? 来表示,其中 ??? 是十六进制的码点值。 0-65536(U+0000 – U+FFFF)的码点被称为基本字符区域(BMP)。

JavaScript 中的字符串是永远无法变更的,一旦字符串构造出来,无法用任何方式改变字符串的内容,所以字符串具有值类型的特征。

JavaScript 字符串把每个 UTF16 单元当作一个字符来处理,所以处理非BMP(超出 U+0000 – U+FFFF 范围)的字符时,你应该格外小心。

JavaScript 这个设计继承自 Java,最新标准中是这样解释的,这样设计是为了“性能和尽可能实现起来简单”。因为现实中很少用到 BMP 之外的字符。

Number

下面,我们来说说Number类型。Number类型表示我们通常意义上的“数字”。这个数字大致对应数学中的有理数,当然,在计算机中,我们有一定的精度限制。

JavaScript中的Number类型有 18437736874454810627(即2^64-2^53+3) 个值。

JavaScript 中的 Number 类型基本符合 IEEE 754-2008 规定的双精度浮点数规则,但是JavaScript为了表达几个额外的语言场景(比如不让除以0出错,而引入了无穷大的概念),规定了几个例外情况:

  • NaN,占用了 9007199254740990,这原本是符合IEEE规则的数字;
  • Infinity,无穷大;
  • -Infinity,负无穷大。

另外,值得注意的是,JavaScript中有 +0 和 -0,在加法类运算中它们没有区别,但是除法的场合则需要特别留意区分,“忘记检测除以-0,而得到负无穷大”的情况经常会导致错误,而区分 +0 和 -0 的方式,正是检测 1/x 是 Infinity 还是 -Infinity。

根据双精度浮点数的定义,Number类型中有效的整数范围是-0x1fffffffffffff至0x1fffffffffffff,所以Number无法精确表示此范围外的整数。

同样根据浮点数的定义,非整数的Number类型无法用 ==(===也不行) 来比较,一段著名的代码,这也正是我们第三题的问题,为什么在JavaScript中,0.1+0.2不能=0.3:

  console.log( 0.1 + 0.2 == 0.3);

这里输出的结果是false,说明两边不相等的,这是浮点运算的特点,也是很多同学疑惑的来源,浮点数运算的精度问题导致等式左右的结果并不是严格相等,而是相差了个微小的值。

所以实际上,这里错误的不是结论,而是比较的方法,正确的比较方法是使用JavaScript提供的最小精度值:

  console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON);

检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。这段代码结果就是 true 了。

Symbol

Symbol 是 ES6 中引入的新类型,它是一切非字符串的对象key的集合,在ES6规范中,整个对象系统被用Symbol 重塑。

在后面的文章中,我会详细叙述 Symbol 跟对象系统。这里我们只介绍Symbol类型本身:它有哪些部分,它表示什么意思,以及如何创建Symbol类型。

Symbol 可以具有字符串类型的描述,但是即使描述相同,Symbol也不相等。

我们创建 Symbol 的方式是使用全局的 Symbol 函数。例如:

    var mySymbol = Symbol("my symbol");

一些标准中提到的 Symbol,可以在全局的 Symbol 函数的属性中找到。例如,我们可以使用 Symbol.iterator 来自定义 for…of 在对象上的行为:

    var o = new Object

    o[Symbol.iterator] = function() {
        var v = 0
        return {
            next: function() {
                return { value: v++, done: v > 10 }
            }
        }        
    };

    for(var v of o) 
        console.log(v); // 0 1 2 3 ... 9

代码中我们定义了iterator之后,用for(var v of o)就可以调用这个函数,然后我们可以根据函数的行为,产生一个for…of的行为。

这里我们给对象o添加了 Symbol.iterator 属性,并且按照迭代器的要求定义了一个0到10的迭代器,之后我们就可以在for of中愉快地使用这个o对象啦。

这些标准中被称为“众所周知”的 Symbol,也构成了语言的一类接口形式。它们允许编写与语言结合更紧密的 API。

Object

Object 是 JavaScript 中最复杂的类型,也是 JavaScript 的核心机制之一。Object表示对象的意思,它是一切有形和无形物体的总称。

下面我们来看一看,为什么给对象添加的方法能用在基本类型上?

在 JavaScript 中,对象的定义是“属性的集合”。属性分为数据属性和访问器属性,二者都是key-value结构,key可以是字符串或者 Symbol类型。

关于对象的机制,后面会有单独的一篇来讲述,这里我重点从类型的角度来介绍对象类型。

提到对象,我们必须要提到一个概念:类。

因为 C++ 和 Java 的成功,在这两门语言中,每个类都是一个类型,二者几乎等同,以至于很多人常常会把JavaScript的“类”与类型混淆。

事实上,JavaScript 中的“类”仅仅是运行时对象的一个私有属性,而JavaScript中是无法自定义类型的。

JavaScript中的几个基本类型,都在对象类型中有一个“亲戚”。它们是:

  • Number;
  • String;
  • Boolean;
  • Symbol。

所以,我们必须认识到 3 与 new Number(3) 是完全不同的值,它们一个是 Number 类型, 一个是对象类型。

Number、String和Boolean,三个构造器是两用的,当跟 new 搭配时,它们产生对象,当直接调用时,它们表示强制类型转换。

Symbol 函数比较特殊,直接用 new 调用它会抛出错误,但它仍然是 Symbol 对象的构造器。

JavaScript 语言设计上试图模糊对象和基本类型之间的关系,我们日常代码可以把对象的方法在基本类型上使用,比如:

    console.log("abc".charAt(0)); //a

甚至我们在原型上添加方法,都可以应用于基本类型,比如以下代码,在 Symbol 原型上添加了hello方法,在任何 Symbol 类型变量都可以调用。

    Symbol.prototype.hello = () => console.log("hello");

    var a = Symbol("a");
    console.log(typeof a); //symbol,a并非对象
    a.hello(); //hello,有效

所以我们文章开头的问题,答案就是. 运算符提供了装箱操作,它会根据基础类型构造一个临时对象,使得我们能在基础类型上调用对应对象的方法。

类型转换

讲完了基本类型,我们来介绍一个现象:类型转换。

因为JS是弱类型语言,所以类型转换发生非常频繁,大部分我们熟悉的运算都会先进行类型转换。大部分类型转换符合人类的直觉,但是如果我们不去理解类型转换的严格定义,很容易造成一些代码中的判断失误。

其中最为臭名昭著的是JS中的“ == ”运算,因为试图实现跨类型的比较,它的规则复杂到几乎没人可以记住。

这里我们当然也不打算讲解==的规则,它属于设计失误,并非语言中有价值的部分,很多实践中推荐禁止使用“ ==”,而要求程序员进行显式地类型转换后,用 === 比较。

其它运算,如加减乘除大于小于,也都会涉及类型转换。幸好的是,实际上大部分类型转换规则是非常简单的,如下表所示:

在这个里面,较为复杂的部分是Number和String之间的转换,以及对象跟基本类型之间的转换。我们分别来看一看这几种转换的规则。

StringToNumber

字符串到数字的类型转换,存在一个语法结构,类型转换支持十进制、二进制、八进制和十六进制,比如:

  • 30;
  • 0b111;
  • 0o13;
  • 0xFF。

此外,JavaScript支持的字符串语法还包括正负号科学计数法,可以使用大写或者小写的e来表示:

  • 1e3;
  • -1e-2。

需要注意的是,parseInt 和 parseFloat 并不使用这个转换,所以支持的语法跟这里不尽相同。

在不传入第二个参数的情况下,parseInt只支持16进制前缀“0x”,而且会忽略非数字字符,也不支持科学计数法。

在一些古老的浏览器环境中,parseInt还支持0开头的数字作为8进制前缀,这是很多错误的来源。所以在任何环境下,都建议传入parseInt的第二个参数,而parseFloat则直接把原字符串作为十进制来解析,它不会引入任何的其他进制。

多数情况下,Number 是比 parseInt 和 parseFloat 更好的选择。

NumberToString

在较小的范围内,数字到字符串的转换是完全符合你直觉的十进制表示。当Number绝对值较大或者较小时,字符串表示则是使用科学计数法表示的。这个算法细节繁多,我们从感性的角度认识,它其实就是保证了产生的字符串不会过长。

具体的算法,你可以去参考JavaScript的语言标准。由于这个部分内容,我觉得在日常开发中很少用到,所以这里我就不去详细地讲解了。

装箱转换

每一种基本类型Number、String、Boolean、Symbol在对象中都有对应的类,所谓装箱转换,正是把基本类型转换为对应的对象,它是类型转换中一种相当重要的种类。

前文提到,全局的 Symbol 函数无法使用 new 来调用,但我们仍可以利用装箱机制来得到一个 Symbol 对象,我们可以利用一个函数的call方法来强迫产生装箱。

我们定义一个函数,函数里面只有return this,然后我们调用函数的call方法到一个Symbol类型的值上,这样就会产生一个symbolObject。

我们可以用console.log看一下这个东西的type of,它的值是object,我们使用symbolObject instanceof 可以看到,它是Symbol这个类的实例,我们找它的constructor也是等于Symbol的,所以我们无论从哪个角度看,它都是Symbol装箱过的对象:

    var symbolObject = (function(){ return this; }).call(Symbol("a"));

    console.log(typeof symbolObject); //object
    console.log(symbolObject instanceof Symbol); //true
    console.log(symbolObject.constructor == Symbol); //true

装箱机制会频繁产生临时对象,在一些对性能要求较高的场景下,我们应该尽量避免对基本类型做装箱转换。

使用内置的 Object 函数,我们可以在JavaScript代码中显式调用装箱能力。

    var symbolObject = Object(Symbol("a"));

    console.log(typeof symbolObject); //object
    console.log(symbolObject instanceof Symbol); //true
    console.log(symbolObject.constructor == Symbol); //true

每一类装箱对象皆有私有的 Class 属性,这些属性可以用 Object.prototype.toString 获取:

    var symbolObject = Object(Symbol("a"));

    console.log(Object.prototype.toString.call(symbolObject)); //[object Symbol]

在 JavaScript 中,没有任何方法可以更改私有的 Class 属性,因此Object.prototype.toString 是可以准确识别对象对应的基本类型的方法,它比 instanceof 更加准确。

但需要注意的是,call本身会产生装箱操作,所以需要配合 typeof 来区分基本类型还是对象类型。

拆箱转换

在JavaScript标准中,规定了 ToPrimitive 函数,它是对象类型到基本类型的转换(即,拆箱转换)。

对象到 String 和 Number 的转换都遵循“先拆箱再转换”的规则。通过拆箱转换,把对象变成基本类型,再从基本类型转换为对应的 String 或者 Number。

拆箱转换会尝试调用 valueOf 和 toString 来获得拆箱后的基本类型。如果 valueOf 和 toString 都不存在,或者没有返回基本类型,则会产生类型错误 TypeError。

    var o = {
        valueOf : () => {console.log("valueOf"); return {}},
        toString : () => {console.log("toString"); return {}}
    }

    o * 2
    // valueOf
    // toString
    // TypeError

我们定义了一个对象o,o有valueOf和toString两个方法,这两个方法都返回一个对象,然后我们进行o*2这个运算的时候,你会看见先执行了valueOf,接下来是toString,最后抛出了一个TypeError,这就说明了这个拆箱转换失败了。

到 String 的拆箱转换会优先调用 toString。我们把刚才的运算从o*2换成 String(o),那么你会看到调用顺序就变了。

    var o = {
        valueOf : () => {console.log("valueOf"); return {}},
        toString : () => {console.log("toString"); return {}}
    }

   String(o)
    // toString
    // valueOf
    // TypeError

在 ES6 之后,还允许对象通过显式指定 @@toPrimitive Symbol 来覆盖原有的行为。

    var o = {
        valueOf : () => {console.log("valueOf"); return {}},
        toString : () => {console.log("toString"); return {}}
    }

    o[Symbol.toPrimitive] = () => {console.log("toPrimitive"); return "hello"}


    console.log(o + "")
    // toPrimitive
    // hello

结语

在本篇文章中,我们介绍了 JavaScript 运行时的类型系统。这里回顾一下今天讲解的知识点。

除了这七种语言类型,还有一些语言的实现者更关心的规范类型。

  • List 和 Record: 用于描述函数传参过程。
  • Set:主要用于解释字符集等。
  • Completion Record:用于描述异常、跳出等语句执行过程。
  • Reference:用于描述对象属性访问、delete等。
  • Property Descriptor:用于描述对象的属性。
  • Lexical Environment 和 Environment Record:用于描述变量和作用域。
  • Data Block:用于描述二进制数据。

有一个说法是:程序 = 算法 + 数据结构,运行时类型包含了所有 JavaScript 执行时所需要的数据结构的定义,所以我们要对它格外重视。

最后我们留一个实践问题,如果我们不用原生的Number和parseInt,用JS代码实现String到Number的转换,该怎么做呢?请你把自己的代码留言给我吧!


补充阅读

事实上,“类型”在 JavaScript 中是一个有争议的概念。一方面,标准中规定了运行时数据类型; 另一方面,JS语言中提供了 typeof 这样的运算,用来返回操作数的类型,但 typeof 的运算结果,与运行时类型的规定有很多不一致的地方。我们可以看下表来对照一下。

在表格中,多数项是对应的,但是请注意object——Null和function——Object是特例,我们理解类型的时候需要特别注意这个区别。

从一般语言使用者的角度来看,毫无疑问,我们应该按照 typeof 的结果去理解语言的类型系统。但JS之父本人也在多个场合表示过,typeof 的设计是有缺陷的,只是现在已经错过了修正它的时机。

04-HTML语义:如何运用语义类标签来呈现Wiki网页?

勤与奋阅读(108)

好,我是winter,今天我们继续来聊聊HTML模块的语义类标签。

在上一篇文章中,我花了大量的篇幅和你解释了正确使用语义类标签的好处和一些场景。那么,哪些场景适合用到语义类标签呢,又如何运用语义类标签呢?

不知道你还记不记得在大学时代,你被导师逼着改毕业论文格式的情景,如果你回想一下,你在论文中使用的那些格式,你会发现其实它们都是可以用HTML里的语义标签来表示的。

这正是因为HTML最初的设计场景就是“超文本”,早期HTML工作组的专家都是出版界书籍排版的专家。

所以,在这一部分,我们找了个跟论文很像的案例:wikipedia文章,这种跟论文相似的网站比较适合用来学习语义类标签。通过分析一篇wiki的文章用到的语义类标签,来进一步帮你理解语义的概念。

你可以在电脑上,打开这个页面:https://en.wikipedia.org/wiki/World_Wide_Web

(为了防止这个页面被修改,我们保存了一个副本:
http://static001.geekbang.org/static/time/quote/World_Wide_Web-Wikipedia.html

这是一篇我们选择的wiki文章,虽然在原本的wikipedia网站中,也是大量使用了div和span来完成功能。在这里,我们来尝试分析一下,应该如何用语义类标签来呈现这样的一个页面/文章。

我们看一下这个页面。

aside

首先我们来看下,左侧侧边栏,根据上一篇文章中提到的语义定义,这里属于aside内容。是导航性质的工具内容。

article

我们来到文章主体部分,因为主体部分具有明确的独立性,所以可以用article来包裹。

hgroup, h1, h2

在语义的上一篇文章中,我们介绍过hgroup和h1-h6的作用,hgroup是标题组,h1是一级标题,h2是二级标题。这里,World Wide Web 是文章的大标题,适合h1元素。

接下来出现了一个副标题。From Wikipedia, the free encyclopedia。这个地方适合使用h2,跟h1组成一个hgroup,所以代码可能是类似这样的:

<hgroup>
<h1>World Wide Web </h1>
<h2>From Wikipedia, the free encyclopedia</h2>
</hgroup>

abbr

abbr标签表示缩写。考虑到WWW是World Wide Web的缩写,所以文中所有出现的WWW,都应该使用abbr标签,

<abbr title="World Wide Web">WWW</abbr>.

hr

细心的同学会发现,在wiki的界面中,出现了一条很长的横线,大家都知道hr标签表示横向分隔线,那么这个地方是不是应该用hr呢?

答案是不用。我们读一下标准的定义就知道了,hr表示故事走向的转变或者话题的转变,显然此处两个标题并非这种关系,所以我们应该使用CSS的border来把它当作纯视觉效果来实现,所以这里是不需要用hr的。

p

接下来一段,我们看到了三段“note”,也就是注记。它在文章中用作额外注释。

“WWW” and “The Web” redirect here. For other uses of WWW, see WWW (disambiguation). For other uses of web, see Web (disambiguation).

For the first web software, see WorldWideWeb.

Not to be confused with the Internet.

HTML中并没有note相关的语义,所以,我们用普通的p标签,加上class="note"来实现。后面的多数自然段都是普通的段落,我们用p标签来实现。

strong

注意,这里 “World Wide Web (WWW)” 和 “the Web” 使用了黑体呈现,从上下文来看,这里表示这个词很重要,所以我们使用strong标签。

<p> 
A global map of the web index for countries in 2014
<strong>The World Wide Web (WWW)</strong>, also called <strong>the Web</strong>,
......

blockquote, q, cite

接下来我们看到了一个论文中很常见的用法“引述”。

interlinked by hypertext links, and accessible via the Internet.[1]

注意看这里的[1],当我们把鼠标放上去的时候,出现了引述的相关信息:

“What is the difference between the Web and the Internet?”. W3C Help and FAQ. W3C. 2009. Archived from the original on 9 July 2015. Retrieved 16 July 2015.

在HTML中,有三个跟引述相关的标签blockquote表示段落级引述内容,q表示行内的引述内容,cite表示引述的作品名。

这里的作品名称 “What is the difference between the Web and the Internet?”,应当使用cite标签。

<cite>"What is the difference between the Web and the Internet?"</cite>. W3C Help and FAQ. W3C. 2009. Archived from the original on 9 July 2015. Retrieved 16 July 2015.

在文章的结尾处,有对应的 References 一节,这一节中所有的作品名称也应该加入cite标签。

这里我们看看引用的原文就可以知道,wiki文章中的信息并非直接引用,如果是直接引用的内容,那么,我们还应该加上blockquote或者q标签。

time

这里除了引用的文章外,还出现了日期,为了让机器阅读更加方便,可以加上time标签:

<cite>"What is the difference between the Web and the Internet?"</cite>. W3C Help and FAQ. W3C. 2009. Archived from the original on <time datetime="2015-07-09">9 July 2015</time>. Retrieved <time datetime="2015-07-16">16 July 2015</time>.

figure, figcaption

我们注意一下文章的右侧,出现了几张图片,这种出现在文中的图片,不仅仅是一个img标签,它和下面的文字组成了一个figure的语法现象,figure也是我们的一种标签(用于表示与主文章相关的图像、照片等流内容)。

<figure>
 <img src="https://.....440px-NeXTcube_first_webserver.JPG"/>
 <figcaption>The NeXT Computer used by Tim Berners-Lee at CERN.</figcaption>
</figure>

这种插入文章中的内容,不仅限图片,代码、表格等,只要是具有一定自包含性(类似独立句子)的内容,都可以用figure。这里面,我们用figcaption表示内容的标题,当然,也可以没有标题。

dfn

然后我们继续往下看,来注意这一句:

The terms Internet and World Wide Web are often used without much distinction. However, the two are not the same. The Internet is a global system of interconnected computer networks. In contrast, the World Wide Web is a global collection of documents and other resources, linked by hyperlinks and URIs.

这里分别定义了Internet和World Wide Web,我们应该使用dfn标签。


The terms Internet and World Wide Web are often used without much distinction. However, the two are not the same. 
The <dfn>Internet</dfn> is a global system of interconnected computer networks.
In contrast, the <dfn>World Wide Web</dfn> is a global collection of documents and other resources, linked by hyperlinks and URIs. 

代码中你可以看见,你需要在你要定义的词前后放上dfn标签,所以我们知道了,dfn标签是用来包裹被定义的名词。

nav, ol, ul

接下来,几个普通的段落之后,我们看到了文章的目录。这里的目录链接到文章的各个章节,我们可以使用nav标签。因为这里的目录顺序不可随意变化,所以我们这里使用多级的ol结构。

<nav>
  <h2>Contents</h2>
  <ol>
    <li><a href="...">History</a></li>
    <li><a href="...">Function</a>
      <ol>
        <li><a href="...">Linking</a></li>
        <li><a href="...">Dynamic updates of web pages</a></li>
        ...
      </ol>
    </li>
    ...
  </ol>
</nav>

我们这里必须要指出,ol和ul的区分是内容是否有顺序关系,每一项的前面不论是数字还是点,都不会影响语义的判断。所以,你可以注意一下这里,不要因为视觉表现效果,而改变语义的使用。

pre, samp, code

继续往下,我们来到了这里,我们看见这篇文章有一个很重要的特色,文章中嵌入了一些代码和一些预先编写好的段落。我们看到在“Function”小节中有一段背景色是灰色的文字。

GET /home.html HTTP/1.1
Host: www.example.org

这是一段HTTP协议的内容描述,因为这段内容的换行是非常严格的,所以我们不需要浏览器帮我们做自动换行,因此我们使用了pre标签,表示这部分内容是预先排版过的,不需要浏览器进行排版。

又因为这是一段计算机程序的示例输出,所以我们可以使用samp标签:

<pre><samp>
GET /home.html HTTP/1.1
Host: www.example.org
</samp></pre>

接下来Wiki中的内容出现了一段HTML代码,我们同样不希望浏览器做自动换行。

<html>
  <head>
    <title>Example.org – The World Wide Web</title>
  </head>
  <body>
    <p>The World Wide Web, abbreviated as WWW and commonly known ...</p>
  </body>
</html>

因为同时是代码,我们还需要加上code标签。最后的代码是pre标签包裹了code标签,code标签包裹了HTML代码。

<pre><code>
&lt;html&gt;
  &lt;head&gt;
    &lt;title&gt;Example.org – The World Wide Web&lt;/title&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;p&gt;The World Wide Web, abbreviated as WWW and commonly known ...&lt;/p&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre>

在后面的代码中,还有一些在行内的code,比如 title和 p括起来的内容,这些也都应该使用code标签。

总结

在这一篇Wiki文章中,已经涉及了大部分语义标签,可见HTML工作组对语义标签的增加是非常谨慎和保守的。

当然了,我们选择的案例不可能刚巧覆盖所有的标签,还有些没讲到的标签,我们这里稍微做一下简要的补充说明。

(长按点击大图查看)

实际上,HTML这种语言,并不像严谨的编程语言一样,有一条非此即彼的线。一些语义的使用其实会带来争议,所以我的建议是:你可以尽量只用自己熟悉的语义标签,并且只在有把握的场景引入语义标签。这样,我们才能保证语义标签不被滥用,造成更多的问题。

你最擅长使用哪些语义标签,会把它们用在哪些场景里呢?欢迎留言告诉我,我们一起讨论。

03-HTML语义:div和span不是够用了吗?

勤与奋阅读(130)

你好,我是winter。

今天这篇是我们正式开篇的第一篇文章,我想和你聊聊HTML。

我猜屏幕那一边的你估计会说:“HTML我很熟悉了,每天写,这不是初级程序员才学的内容么,这我还能不会吗?”

其实在我看来,HTML并不简单,它是典型的“入门容易,精通困难”的一部分知识。深刻理解HTML是成为优秀的前端工程师重要的一步。

我们在上一篇文章中讲到了,HTML的标签可以分为很多种,比如head里面的元信息类标签,又比如img、video、audio之类的替换型媒体标签。我今天要讲的标签是:语义类标签。

语义类标签是什么,使用它有什么好处?

语义类标签也是大家工作中经常会用到的一类标签,它们的特点是视觉表现上互相都差不多,主要的区别在于它们表示了不同的语义,比如大家会经常见到的section、nav、p,这些都是语义类的标签。

语义是我们说话表达的意思,多数的语义实际上都是由文字来承载的。语义类标签则是纯文字的补充,比如标题、自然段、章节、列表,这些内容都是纯文字无法表达的,我们需要依靠语义标签代为表达。

在讲语义之前,我们来说说为什么要用语义。

现在我们很多的前端工程师写起代码来,多数都不用复杂的语义标签, 只靠div 和 span 就能走天下了。

这样做行不行呢?毫无疑问答案是行。那这样做好不好呢?按照正确的套路,我应该说不好,但是在很多情况下,答案其实是好。

这是因为在现代互联网产品里,HTML用于描述“软件界面”多过于“富文本”,而软件界面里的东西,实际上几乎是没有语义的。比如说,我们做了一个购物车功能,我们一定要给每个购物车里的商品套上ul吗?比如说,加入购物车这个按钮,我们一定要用Button吗?

实际上我觉得没必要,因为这个场景里面,跟文本中的列表,以及表单中的Button,其实已经相差很远了,所以,我支持在任何“软件界面”的场景中,直接使用div和span。

不过,在很多工作场景里,语义类标签也有它们自己无可替代的优点。正确地使用语义标签可以带来很多好处。

  • 语义类标签对开发者更为友好,使用语义类标签增强了可读性,即便是在没有CSS的时候,开发者也能够清晰地看出网页的结构,也更为便于团队的开发和维护。
  • 除了对人类友好之外,语义类标签也十分适宜机器阅读。它的文字表现力丰富,更适合搜索引擎检索(SEO),也可以让搜索引擎爬虫更好地获取到更多有效信息,有效提升网页的搜索量,并且语义类还可以支持读屏软件,根据文章可以自动生成目录等等。

不过,不恰当地使用语义标签,反而会造成负面作用。这里我们举一个常见的误区作为例子。我们都知道ul是无序列表,ol是有序列表,所以很多接触过语义这个概念,半懂不懂的前端工程师,特别喜欢给所有并列关系的元素都套上ul。

实际上, ul 是长成下面的这种样子的(以下来自HTML标准)。

I have lived in the following countries:

  • Switzerland
  • Norway
  • United Kingdom
  • United States

ul多数出现正在行文中间,它的上文多数在提示:要列举某些项。但是,如果所有并列关系都用ul,会造成大量冗余标签。

错误地使用语义标签,会给机器阅读造成混淆、增加嵌套,给CSS编写加重负担。

所以,对于语义标签,我的态度是:“用对”比“不用”好,“不用”比“用错”好。当然了,我觉得有理想的前端工程师还是应该去追求“用对”它们。


与JavaScript这样严格的编程语言相比,HTML中语义标签的使用更接近我们平常说话用的自然语言。我们说话并没有唯一的标准措辞,语义标签的使用也是一样。下面,我挑选了几种(我认为)比较重要的语义标签使用场景,来为你介绍一下。

作为自然语言延伸的语义类标签

其实语义问题不仅仅属于理科,它还是个文科问题。

所以我们这里讲语义标签的使用的第一个场景,也是最自然的使用场景,就是:作为自然语言和纯文本的补充,用来表达一定的结构或者消除歧义。

我们先来看看“表达一定的结构”这个场景。

在日语中,有一个语法现象叫做:ルビ,它的读音是ruby(著名的ruby语言就是据此命名的),它中文的意思大约类似于注音或者意思的注解,它的形式可以看下图:

图中的例子选自动画片《某科学的超电磁炮》第二季第一话。图中把teleport放在空间移动上方的用法,就是日文中ruby的用法。“空间移动”是动画中白井黑子的技能,这里动画字幕上写的是“空间移动”,动画里的台词则用了英文发音“Teleport”,这里就形成了一个使用ruby的场景。

ruby的这个形式,在中国的网友中间最近被玩出了新花样,比如表情包。

有时候微信聊天,不能用ruby这样的东西真的是好急啊,只好用括号代替,效果真是差了不少。

在HTML5中,就引入了这个表示ruby的标签,它由ruby、rt、rp三个标签来实现。

所以说,这些情况里存在的语义,其实原本就存在了,只是我们用纯文字是没法表达的,HTML作为一种“超文本”语言,支持这些文字表达就是必要的了。

还有一种情况是,HTML的有些标签实际上就是必要的,甚至必要的程度可以达到:如果没有这个标签,文字会产生歧义的程度。

这里我们可以介绍一下em标签。

今天我吃了一个苹果.

我们看看这句话,看上去它很清楚,但是实际上,这句话放到不同上下文中,可能表达完全不同的意思。

昨天我吃了一个香蕉。
今天我吃了一个苹果。

再比如:

昨天我吃了两个苹果。
今天我吃了一个苹果。

试着读一读,这两段里面的“今天我吃了一个苹果”,你是不是发现读音不自觉地发生了变化?

实际上,不仅仅是读音,这里的意思也发生了变化。前一段中,表示我今天吃的是苹果,而不是别的什么东西,后一段中,则表示我今天只吃了一个苹果,没有多吃。

当没有上下文时,如何消除歧义呢?这就要用到我们的em标签了。em表示重音:

今天我吃了一个<em>苹果</em>。
今天我吃了<em>一个</em>苹果。

通过em标签,我们可以消除这样的歧义。

一些文章常常会拿em和strong做对比,实际上,我们只要理解了em的真正意思,它和strong可谓天差地别,并没有任何混淆的可能。

作为标题摘要的语义类标签

介绍完自然语言的语义场景后,我想介绍的另一个语义重要使用场景,就是文章的结构。中国古代小说就形成了“章-回”的概念,西方的戏剧也有幕的区分,所以人类的自然语言作品也是如出一辙。

HTML也应该支持这样的需求。HTML语义标签中,有不少是用于支持这样的结构的标签。

语义化的HTML能够支持自动生成目录结构,HTML标准中还专门规定了生成目录结构的算法,即使我们并不打算深入实践语义,也应该尽量在大的层面上保证这些元素的语义化使用。

首先我们需要形成一个概念,一篇文档会有一个树形的目录结构,它由各个级别的标题组成。这个树形结构可能不会跟HTML元素的嵌套关系一致。

例如:

<h1>HTML语义</h1>
<p>balah balah balah balah</p>
<h2>弱语义</h2>
<p>balah balah</p>
<h2>结构性元素</h2>
<p>balah balah</p>
......

这段HTML几乎是平铺的元素,但是它的标题结构是:

  • HTML语义
    • 弱语义
    • 结构性元素
    • ……

h1-h6是最基本的标题,它们表示了文章中不同层级的标题。有些时候,我们会有副标题,为了避免副标题产生额外的一个层级,我们使用hgroup标签。

我们来看下有/无hgroup的对比:

<h1>JavaScript对象</h1>
<h2>我们需要模拟类吗?</h2>
<p>balah balah</p>
......

此段生成以下标题结构:

  • JavaScript对象
    • 我们需要模拟类吗?
<hgroup>
<h1>JavaScript对象</h1>
<h2>我们需要模拟类吗?</h2>
</hgroup>
<p>balah balah</p>
......

这一段生成以下标题结构:

  • JavaScript对象——我们需要模拟类吗?

我们通过两个效果的对比就可以知道,在hgroup中的h1-h6被视为同一标题的不同组成部分。

从HTML 5开始,我们有了section标签,这个标签可不仅仅是一个“有语义的div”,它会改变h1-h6的语义。section的嵌套会使得其中的h1-h6下降一级,因此,在HTML5以后,我们只需要section和h1就足以形成文档的树形结构:

<section>
    <h1>HTML语义</h1>
    <p>balah balah balah balah</p>
    <section>
        <h1>弱语义</h1>
        <p>balah balah</p>
    </section>
    <section>
        <h1>结构性元素</h1>
        <p>balah balah</p> 
    </section>
......
</section>

这段代码同样会形成前面例子的标题结构:

  • HTML语义
    • 弱语义
    • 结构性元素
    • ……

作为整体结构的语义类标签

我们想介绍的最后一个场景是,随着越来越多的浏览器推出“阅读模式”,以及各种非浏览器终端的出现,语义化的HTML适合机器阅读的特性变得越来越重要。

应用了语义化结构的页面,可以明确地提示出页面信息的主次关系,它能让浏览器很好地支持“阅读视图功能”,还可以让搜索引擎的命中率提升,同时,它也对视障用户的读屏软件更友好。

我们正确使用整体结构类的语义标签,可以让页面对机器更友好。比如,这里一个典型的body类似这样:

<body>
    <header>
        <nav>
            ……
        </nav>
    </header>
    <aside>
        <nav>
            ……
        </nav>
    </aside>
    <section>……</section>
    <section>……</section>
    <section>……</section>
    <footer>
        <address>……</address>
    </footer>
</body>

在body下面,有一个header,header里面是一个nav,跟header同级的有一个aside,aside里面也有一个nav。接下来是文章的整体,也就是一个一个的section。section里面可能还有嵌套,但是我们就不管了,最后是一个footer,这个footer里面可能有address这样的内容。

除此之外,还有article,article是一种特别的结构,它表示具有一定独立性质的文章。所以,article和body具有相似的结构,同时,一个HTML页面中,可能有多个article存在。

一个典型的场景是多篇新闻展示在同一个新闻专题页面中,这种类似报纸的多文章结构适合用article来组织。

<body>
    <header>……</header>
    <article>
        <header>……</header>
        <section>……</section>
        <section>……</section>
        <section>……</section>
        <footer>……</footer>
    </article>
    <article>
        ……
    </article>
    <article>
        ……
    </article>
    <footer>
        <address></address>
    </footer>
</body>

body里面有自己的header和footer,然后里面是竖篇的article,每一个article里面都有自己的header、section、footer。这是一个典型的多文章结构。

在这个结构里,我们看到了一些新标签,我也来逐个介绍一下。

  • header,如其名,通常出现在前部,表示导航或者介绍性的内容。
  • footer,通常出现在尾部,包含一些作者信息、相关链接、版权信息等。

header和footer一般都是放在article或者body的直接子元素,但是标准中并没有明确规定,footer也可以和aside,nav,section相关联(header不存在关联问题)。

  • aside表示跟文章主体不那么相关的部分,它可能包含导航、广告等工具性质的内容。

aside很容易被理解为侧边栏,实际上二者是包含关系,侧边栏是aside,aside不一定是侧边栏。

aside和header中都可能出现导航(nav标签),二者的区别是,header中的导航多数是到文章自己的目录,而aside中的导航多数是到关联页面或者是整站地图。

最后footer中包含address,这是个非常容易被误用的标签。address并非像date一样,表示一个给机器阅读的地址,而是表示“文章(作者)的联系方式”,address明确地只关联到article和body。

总结

本篇中我们介绍了一些基本原则和HTML文档的整体结构,从整体上了解了HTML语义。

至此,我们可以回答是否要语义化的问题:我们应该分开一些场景来看语义,把它用在合适的场景下,可以获得额外的效果。本篇文中,我们至少涉及了三个明确的场景:

  • 自然语言表达能力的补充;
  • 文章标题摘要;
  • 适合机器阅读的整体结构。

下一篇中,我们会继续深入到更细致的结构中,进一步了解语义。你在工作中是否在使用语义化的标签开发?学习过本篇之后,答案有没有变化呢?你可以给我留言,我们一起讨论。

列一份前端知识架构图

勤与奋阅读(107)

你好,我是winter。

在上一篇文章中,我们简要地总结了前端的学习路径与方法,我们提到的第一个学习方法就是:建立知识框架。那么,今天我们就一起来列一份前端的知识框架图。

在开始列框架之前,我想先来谈谈我们的目标。实际上,我们在网上可以找到很多参考资料,比如MDN这样的参考手册,又比如一份语言标准,但是我们的课程既不是一本参考手册,也不是一份语言标准。参考手册希望做到便于查阅、便于理解和全面,语言标准的目标是严谨、无遗漏、无歧义。

而我们的课程有什么不同呢?我认为,作为一个课程,有两个目标:一个是把无法通过查阅解决的原理和背景讲清楚,另一个是把不方便查阅和记忆的内容整理好。

我会尽量避免像前面提到的两种文档一样逐条目罗列知识点和细节,当然,这不是在说两种文档没有价值,而是我们各有分工,参考手册和语言标准做的事情,我们没必要重复去做,即使做了也不一定能做得更好。

在这个课程里,我希望能和你一起打造一个前端知识的框架,再把知识点做个遍历,这其中,有原理和背景的部分,我去讲解知识的原理和背景。如果没有的话,我们就去讲整理和记忆这部分知识的方法,这样,即使你遇见无法一下子记住的知识,也可以很容易地查阅参考手册和标准来解决。

如果让我做一个划分,前端的知识在总体上分成基础部分和实践部分,基础部分包含了JavaScript语言(模块一)、CSS和HTML(模块二)以及浏览器的实现原理和API(模块三),这三个模块涵盖了一个前端工程师所需要掌握的全部知识。

学完这三个部分,你再结合基本的编程能力,就可以应对基本的前端开发工作了。实践部分(模块四)重点会介绍我在工作过程中遇到的问题和解决方案,希望这块内容能够帮助你和你的前端团队找到可能的发展方向和着力点。

JavaScript

上面是我整理的JavaScript知识架构图,下面我们来具体解释一下。

在JavaScript的模块中,首先我们可以把语言按照文法、语义和运行时来拆分,这符合编程语言的一般规律:用一定的词法和语法,表达一定语义,从而操作运行时。

接下来,我们又按照程序的一般规律,把运行时分为数据结构和算法部分:数据结构包含类型和实例(JavaScript的类型系统就是它的7种基本类型和7种语言类型,实例就是它的内置对象部分)。所谓的算法,就是JavaScript的执行过程。

类型部分中,对象比其它所有类型加起来都要更为复杂,所以我们会用较长的篇幅来讲解对象,包括它的一些历史和设计思路。

执行过程我们则需要按照从大结构到小结构的角度讲解,从最顶层的程序与模块、事件循环和微任务,到函数、再到语句级的执行。我们从粗到细地了解执行过程。

实例部分,对JavaScript来说类似基础库,JavaScipt的内置对象多达150以上,考虑到我们即使逐次讲解也必定不如MDN更加细致全面,所以我们会从应用和机制的角度,挑选其中几个体系来讲解。

文法中的语法和语义基本是一一对应关系,在JavaScript标准中有一份语法定义表,它同样不适合一一讲解,我们会从JavaScript语法中特别的地方,以及与日常开发比较相关的地方来重点讲解,剩下的内容和词法部分,我们会带领大家做一些数据挖掘工作,从这份表格中找到一些和我们日常开发息息相关的内容。

语义的大部分内容我们会在运行时的讲解中透出,同时它又跟语法有对应的关系,所以我们不再单独拿出来讲解。

HTML 和 CSS

上面是我整理的HTML和CSS的知识架构图,我们来具体解释一下。

在HTML的部分,我们会按照功能和语言来划分它的知识,HTML的功能主要由标签来承担,所以我们首先会把标签做一些分类,并对它们分别进行讲解。

我们都知道HTML的标签可以分为很多种,head里面的我们称为元信息类标签,诸如title、meta、style、link、base这些,它们用来描述文档的一些基本信息。还有一类是一些诸如section、nav的标签,它们在视觉表现上跟div并没有区别,但是各有各的适用场景,我们把它们称作语义类标签。另外一类是img、video、audio之类的替换型媒体类标签,用来引入外部内容,平常开发中你也会经常用到。再有就是表单类的,比如input、button。

所以,基于这样的分类,我把标签分成下面几种。

  1. 文档元信息:通常是出现在head标签中的元素,包含了描述文档自身的一些信息;
  2. 语义相关:扩展了纯文本,表达文章结构、不同语言要素的标签;
  3. 链接:提供到文档内和文档外的链接;
  4. 替换型标签:引入声音、图片、视频等外部元素替换自身的一类标签;
  5. 表单:用于填写和提交信息的一类标签;
  6. 表格:表头、表尾、单元格等表格的结构。

我们的重点会放在前四种标签上,表单和表格较少用到,而且基本以查阅型知识为主,这里就不拿出来讲解了。

除了标签之外,我们还应该把HTML当作一门语言来了解下,当然,标记语言跟编程语言不太一样,没有编程语言那么严谨,所以,我们会简要介绍HTML的语法和几个重要的语言机制:实体、命名空间。

最后我们会介绍下HTML的补充标准:ARIA,它是HTML的扩展,在可访问性领域,它有至关重要的作用。

CSS部分,按照惯例,我们也会从语言和功能两个角度去介绍。在语言部分,我们会从大到小介绍CSS的各种语法结构,比如@rule、选择器、单位等等。功能部分,我们大致可以分为布局、绘制和交互类。

在布局类我们介绍两个最常用的布局:正常流和弹性布局。绘制类我们则会分成图形相关的和文字相关的绘制。最后我们会介绍动画和其它交互。

浏览器的实现原理和API

上面是我整理的浏览器知识架构图,我们来具体看一下。

浏览器部分我们会先介绍下浏览器的实现原理,这是我们深入理解API的基础。

我们会从一般的浏览器设计出发,按照解析、构建DOM树、计算CSS、渲染、合成和绘制的流程来讲解浏览器的工作原理。

在API部分,我们会从W3C零散的标准中挑选几个大块的API来详细讲解,主要有:事件、DOM、CSSOM几个部分,它们分别覆盖了交互、语义和可见效果,这是我们工作中用到的主要内容。

其他的API怎么办呢,别着急,在最后,我会给出一份Chrome已经实现的API跟W3C标准的对应关系和它的生成过程,来覆盖其它部分。

前端工程实践

最后一个模块是前端工程实践。我们在掌握了前面的基础知识之后,也就基本掌握了做一个前端工程师的底层能力。在这个模块中,我选择了性能、工具链、持续集成、搭建系统、架构与基础库这几个方向的前端工程实践案例,来与你一起分享我的经验。

性能

首先我们会谈谈性能。对任何一个前端团队而言,性能是它价值的核心指标,从早年“重构”的实践开始,前端有通过性能证明自己价值的传统。

但是性能并非细节的堆砌,也不是默默做优化,所以,我会从团队的角度来跟你一起探讨性能的方法论和技术体系。

工具链

下一个案例是工具链。这一部分,我将会探讨企业中工具链的建设思路。对一个高效又合作良好的前端团队来说,一致性的工具链是不可或缺的保障,作为开发阶段的入口,工具链又可以和性能、发布、持续集成等系统链接到一起,成为团队技术管理的基础。

持续集成

接下来还会给大家介绍前端的持续集成,持续集成并非一个新概念,但是过去持续集成概念和理论都主要针对软件开发,而对前端来说,持续集成是一个新的课题(当然对持续集成来说,前端也是一个新课题),比如daily build就完全不适用前端,前端代码必须是线上实时可用的。这一部分内容将会针对前端的持续集成提出一些建设的思路。

搭建系统

接下来的案例是搭建系统,前端工作往往多而繁杂,针对高重复性、可模块化的业务需求,传统的人工开发不再适用,搭建系统是大部分大型前端团队的选择。这一部分内容我将会介绍什么是搭建系统,以及一些常见的搭建系统类型。

架构与基础库

最后一个部分,会给大家介绍前端架构和基础库的知识。软件架构师主要解决功能复杂性的问题,服务端架构师主要解决高流量问题,而前端是页面间天然解耦,分散在用户端运行的系统,但是前端架构也有自己要解决的问题。

前端需求量大、专业人才稀缺,更因为前端本身运行在浏览器中,有大量兼容工作要做。所以前端架构的主要职责是兼容性、复用和能力扩展。这一部分文章我将会介绍前端架构工作的一些思路和切入点。

上面的这些案例来自我在领导手淘前端团队时的经验,和我在阿里巴巴工作参与晋升面试时听到的案例,这些内容几乎是每一个年轻的前端团队成长过程中都会需要的基础设施。

好了,前端的知识体系我们大致列出来了。你可能发现了,知识体系图中的每一个知识点,专栏里都有与之对应的文章,这也是我的初衷:希望借由讲解这40余个知识点,帮你建立起前端的知识框架。


(长按保存高清大图)

讲述形式

基于这份知识框架图,我们的课程主要采用两种讲述形式:一种是重点讲解的课程,一种是知识图谱型的课程。

重点讲解的课程我们会从技术的背景、原理和设计出发,把知识的内容呈现出来。这种形式适用于有体系和源流的知识,比较适合系统学习和理解,比如JavaScript中的对象、CSS的排版。

知识图谱型的课程则提供一些方法,用表格或者脑图的形式来整理知识的结构。这种形式适用于零散的知识,比较适合记住大概,用到时去查阅,比如JavaScript的词法、HTML中的所有标签、以及浏览器中的API就十分适合这样的讲解方式。

结语

今天我带你一起划分了前端的知识内容,前端的基础知识分成JavaScript、HTML、CSS以及浏览器四大重点模块,每个模块也分别有自己的技术重点。你可以在框架中,挑选你最需要的前端知识,按需学习。

当然,这篇文章最重要的是,我希望能帮你建立一个理解前端的全景图。这样,任何时候,你都能够体系地思考问题,分析问题。

你觉得你的划分跟我一样吗,你还有其他的想法,你觉得是否有想了解的知识不在其中,欢迎给我留言。

明确你的前端学习路线与方法

勤与奋阅读(95)

01-明确你的前端学习路线与方法

你好,我是winter。今天我们一起来聊聊前端的学习路线与方法。

在“开篇词”中,我和你简单回顾了前端行业的发展,到现在为止,前端工程师已经成为研发体系中的重要岗位之一。可是,与此相对的是,我发现极少或者几乎没有大学的计算机专业愿意开设前端课程,更没有系统性的教学方案出现。大部分前端工程师的知识,其实都是来自于实践和工作中零散的学习。

这样的现状就引发了一系列的问题。

首先是前端的基础知识,常常有一些工作多年的工程师,在看到一些我认为很基础的JavaScript语法的时候,还会惊呼“居然可以这样”。是的,基础知识的欠缺会让你束手束脚,更限制你解决问题的思路。

其次,技术上存在短板,就会导致前端开发者的上升通道不甚顺畅。特别是一些小公司的程序员,只能靠自己摸索,这样就很容易陷入重复性劳动的陷阱,最终耽误自己的职业发展。

除此之外,前端工程师也会面临技术发展问题带来的挑战。前端社区高度活跃,前端标准也在快速更新,这样蓬勃发展对技术来说无疑是好事,但是副作用也显而易见,它使得前端工程师的学习压力变得很大。

我们就拿JavaScript标准来说,ES6中引入的新特性超过了过去十年的总和,新特性带来的实践就更多了,仅仅是一个Proxy特性的引入,就支持了VueJS从2.0到3.0的内核原理完全升级。

缺少系统教育+技术快速革新,在这样的大环境下,前端工程师保持自学能力就显得尤其重要了。

那么,前端究竟应该怎么学呢?我想,我可以简单分享一下自己的经验。

学习路径与学习方法

首先是0基础入门的同学,你可以读几本经典的前端教材,比如《JavaScript高级程序设计》《精通CSS》等书籍,去阅读一些参考性质的网站也是不错的选项,比如MDN

如果你至少已经有了1年以上的工作经验,希望在技术上有一定突破,那么,这个专栏就可以是你技术进阶的一个选项了。

在这个专栏中,我希望传达的不仅仅是具体的知识点,还有体系架构和学习方法。我希望达到三个目标:

  • 带你摸索出适合自己的前端学习方法;
  • 帮助你建立起前端技术的知识架构;
  • 让你理解前端技术背后的核心思想。

在开始具体的知识讲解之前,这篇文章中,我想先来谈两个前端学习方法。

第一个方法:建立知识架构

第一个方法是建立自己的知识架构,并且在这个架构上,不断地进行优化。

我们先来讲讲什么叫做知识架构?我们可以把它理解为知识的“目录”或者索引,它能够帮助我们把零散的知识组织起来,也能够帮助我们发现一些知识上的盲区。

当然,知识的架构是有优劣之分的,最重要的就是逻辑性和完备性。

我们来思考一个问题,如果我们要给JavaScript知识做一个顶层目录,该怎么做呢?

如果我们把一些特别流行的术语和问题,拼凑起来,可能会变成这样:

  • 类型转换;
  • this指针;
  • 闭包;
  • 作用域链;
  • 原型链;
  • ……

这其实不是我们想要的结果,因为这些知识点之间,没有任何逻辑关系。它们既不是并列关系,又不是递进关系,合在一起,也就没有任何意义。这样的知识架构,无法帮助我们去发现问题和理解问题。

如果让我来做,我会这样划分:

  • 文法
  • 语义
  • 运行时

为什么这样分呢,因为对于任何计算机语言来说,必定是“用规定的文法,去表达特定语义,最终操作运行时的”一个过程。

这样,JavaScript的任何知识都不会出现在这个范围之外,这是知识架构的完备性。我们再往下细分一个层级,就变成了这个样子:

  • 文法
    • 词法
    • 语法
  • 语义
  • 运行时
    • 类型
    • 执行过程

我来解释一下这个划分。

文法可以分成词法和语法,这来自编译原理的划分,同样是完备的。语义则跟语法具有一一对应关系,这里暂时不区分。

对于运行时部分,这个划分保持了完备性,我们都知道:程序 = 算法 + 数据结构,那么,对运行时来说,类型就是数据结构,执行过程就是算法。

当我们再往下细分的时候,就会看到熟悉的概念了,词法中有各种直接量、关键字、运算符,语法和语义则是表达式、语句、函数、对象、模块,类型则包含了对象、数字、字符串等……

这样逐层向下细分,知识框架就初见端倪了。在顶层和大结构上,我们通过逻辑来保持完备性。如果继续往下,就需要一些技巧了,我们可以寻找一些线索。

比如在JavaScript标准中,有完整的文法定义,它是具有完备性的,所以我们可以根据它来完成,我们还可以根据语法去建立语义的知识架构。实际上,因为JavaScript有一份统一的标准,所以相对来说不太困难。

如果是浏览器中的API,那就困难了,它们分布在w3c的各种标准当中,非常难找。但是我们要想找到一些具有完备性的线索,也不是没有办法。我喜欢的一个办法,就是用实际的代码去找:for in 遍历window的属性,再去找它的内容。

我想,学习的过程,实际上就是知识架构不断进化的过程,通过知识架构的自然延伸,我们可以更轻松地记忆一些原本难以记住的点,还可以发现被忽视的知识盲点。

建立知识架构,同样有利于面试,没人能够记住所有的知识,当不可避免地谈到一个记不住的知识,如果你能快速定位到它在知识架构中的位置,把一些相关的点讲出来,我想,这也能捞回不少分。(关于前端具体的知识架构,我会在02篇文章中详细讲解。)

第二个方法:追本溯源

第二个方法,我把它称作追本溯源。

有一些知识,背后有一个很大的体系,例如,我们对比一下CSS里面的两个属性:

  • opacity;
  • display。

虽然都是“属性”,但是它们背后的知识量完全不同,opacity是个非常单纯的数值,表达的意思也很清楚,而display的每一个取值背后都是一个不同的布局体系。我们要讲清楚display,就必须关注正常流(Normal Flow)、关注弹性布局系统以及grid这些内容。

还有一些知识,涉及的概念本身经历了各种变迁,变得非常复杂和有争议性,比如MVC,从1979年至今,概念变化非常大,MVC的定义几乎已经成了一段公案,我曾经截取了MVC原始论文、MVP原始论文、微软MSDN、Apple开发者文档,这些内容里面,MVC画的图、箭头和解释都完全不同。

这种时候,就是我们做一些考古工作的时候了。追本溯源,其实就是关注技术提出的背景,关注原始的论文或者文章,关注作者说的话。

操作起来也非常简单:翻翻资料(一般wiki上就有)找找历史上的文章和人物,再顺藤摸瓜翻出来历史资料就可以了,如果翻出来的是历史人物(幸亏互联网的历史不算悠久),你也可以试着发封邮件问问。

这个过程,可以帮助我们理解一些看上去不合理的东西,有时候还可以收获一些趣闻,比如JavaScript之父 Brendan Eich 曾经在Wikipedia的讨论页上解释JavaScript最初想设计一个带有prototype的scheme,结果受到管理层命令把它弄成像Java的样子(如果你再挖的深一点,甚至能找到他对某位“尖头老板”的吐槽)。

根据这么一句话,我们再去看看scheme,看看Java,再看看一些别的基于原型的语言,我们就可以理解为什么JavaScript是现在这个样子了:函数是一等公民,却提供了new this instanceof等特性,甚至抄来了Java的getYear这样的Bug。

结语

今天我带你探索了前端的学习路径,并提出了两个学习方法:你要试着建立自己的知识架构,除此之外,还要学会追本溯源,找到知识的源头。

这个专栏中,我并不奢望通过短短的40篇专栏,事无巨细地把前端的所有知识都罗列清楚,这本身是MDN这样的参考手册的工作。但是,我希望通过这个专栏,把前端技术背后的设计原理和知识体系讲清楚,让你能对前端技术产生整体认知,这样才能够在未来汹涌而来的新技术中保持领先的状态。

在你的认识中,前端知识的结构是怎样的?欢迎留言告诉我,我们一起讨论。

06-JavaScript对象:面向对象还是基于对象?

勤与奋阅读(111)

你好,我是winter。

与其它的语言相比,JavaScript中的“对象”总是显得不那么合群。

一些新人在学习JavaScript面向对象时,往往也会有疑惑:

  • 为什么JavaScript(直到ES6)有对象的概念,但是却没有像其他的语言那样,有类的概念呢;
  • 为什么在JavaScript对象里可以自由添加属性,而其他的语言却不能呢?

甚至,在一些争论中,有人强调:JavaScript并非“面向对象的语言”,而是“基于对象的语言”。这个说法一度流传甚广,而事实上,我至今遇到的持有这一说法的人中,无一能够回答“如何定义面向对象和基于对象”这个问题。

实际上,基于对象和面向对象两个形容词都出现在了JavaScript标准的各个版本当中。

我们可以先看看JavaScript标准对基于对象的定义,这个定义的具体内容是:“语言和宿主的基础设施由对象来提供,并且JavaScript程序即是一系列互相通讯的对象集合”。

这里的意思根本不是表达弱化的面向对象的意思,反而是表达对象对于语言的重要性。

那么,在本篇文章中,我会尝试让你去理解面向对象和JavaScript中的面向对象究竟是什么。

什么是面向对象?

我们先来说说什么是对象,因为翻译的原因,中文语境下我们很难理解“对象”的真正含义。事实上,Object(对象)在英文中,是一切事物的总称,这和面向对象编程的抽象思维有互通之处。

中文的“对象”却没有这样的普适性,我们在学习编程的过程中,更多是把它当作一个专业名词来理解。

但不论如何,我们应该认识到,对象并不是计算机领域凭空造出来的概念,它是顺着人类思维模式产生的一种抽象(于是面向对象编程也被认为是:更接近人类思维模式的一种编程范式)。

那么,我们先来看看在人类思维模式下,对象究竟是什么。

对象这一概念在人类的幼儿期形成,这远远早于我们编程逻辑中常用的值、过程等概念。

在幼年期,我们总是先认识到某一个苹果能吃(这里的某一个苹果就是一个对象),继而认识到所有的苹果都可以吃(这里的所有苹果,就是一个类),再到后来我们才能意识到三个苹果和三个梨之间的联系,进而产生数字“3”(值)的概念。

在《面向对象分析与设计》这本书中,Grady Booch替我们做了总结,他认为,从人类的认知角度来说,对象应该是下列事物之一:

  1. 一个可以触摸或者可以看见的东西;
  2. 人的智力可以理解的东西;
  3. 可以指导思考或行动(进行想象或施加动作)的东西。

有了对象的自然定义后,我们就可以描述编程语言中的对象了。在不同的编程语言中,设计者也利用各种不同的语言特性来抽象描述对象,最为成功的流派是使用“类”的方式来描述对象,这诞生了诸如 C++、Java等流行的编程语言。

而 JavaScript 早年却选择了一个更为冷门的方式:原型(关于原型,我在下一篇文章会重点介绍,这里你留个印象就可以了)。这是我在前面说它不合群的原因之一。

然而很不幸,因为一些公司政治原因,JavaScript推出之时受管理层之命被要求模仿Java,所以,JavaScript创始人Brendan Eich在“原型运行时”的基础上引入了new、this等语言特性,使之“看起来更像Java”。

在 ES6 出现之前,大量的 JavaScript 程序员试图在原型体系的基础上,把JavaScript变得更像是基于类的编程,进而产生了很多所谓的“框架”,比如PrototypeJS、Dojo。

事实上,它们成为了某种JavaScript的古怪方言,甚至产生了一系列互不相容的社群,显然这样做的收益是远远小于损失的。

如果我们从运行时角度来谈论对象,就是在讨论JavaScript实际运行中的模型,这是由于任何代码执行都必定绕不开运行时的对象模型。

不过,幸运的是,从运行时的角度看,可以不必受到这些“基于类的设施”的困扰,这是因为任何语言运行时类的概念都是被弱化的。

首先我们来了解一下JavaScript是如何设计对象模型的。

JavaScript 对象的特征

在我看来,不论我们使用什么样的编程语言,我们都先应该去理解对象的本质特征(参考Grandy Booch《面向对象分析与设计》)。总结来看,对象有如下几个特点。

  • 对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。
  • 对象有状态:对象具有状态,同一对象可能处于不同状态之下。
  • 对象具有行为:即对象的状态,可能因为它的行为产生变迁。

我们先来看第一个特征,对象具有唯一标识性。一般而言,各种语言的对象唯一标识性都是用内存地址来体现的, 对象具有唯一标识的内存地址,所以具有唯一的标识。

所以,JavaScript程序员都知道,任何不同的JavaScript对象其实是互不相等的,我们可以看下面的代码,o1和o2初看是两个一模一样的对象,但是打印出来的结果却是false。

    var o1 = { a: 1 };
    var o2 = { a: 1 };
    console.log(o1 == o2); // false

关于对象的第二个和第三个特征“状态和行为”,不同语言会使用不同的术语来抽象描述它们,比如C++中称它们为“成员变量”和“成员函数”,Java中则称它们为“属性”和“方法”。

在 JavaScript中,将状态和行为统一抽象为“属性”,考虑到 JavaScript 中将函数设计成一种特殊对象(关于这点,我会在后面的文章中详细讲解,此处先不用细究),所以 JavaScript中的行为和状态都能用属性来抽象。

下面这段代码其实就展示了普通属性和函数作为属性的一个例子,其中o是对象,d是一个属性,而函数f也是一个属性,尽管写法不太相同,但是对JavaScript来说,d和f就是两个普通属性。

    var o = { 
        d: 1,
        f() {
            console.log(this.d);
        }    
    };

所以,总结一句话来看,在JavaScript中,对象的状态和行为其实都被抽象为了属性。如果你用过Java,一定不要觉得奇怪,尽管设计思路有一定差别,但是二者都很好地表现了对象的基本特征:标识性、状态和行为。

在实现了对象基本特征的基础上, 我认为,JavaScript中对象独有的特色是:对象具有高度的动态性,这是因为JavaScript赋予了使用者在运行时为对象添改状态和行为的能力。

我来举个例子,比如,JavaScript 允许运行时向对象添加属性,这就跟绝大多数基于类的、静态的对象设计完全不同。如果你用过Java或者其它别的语言,肯定会产生跟我一样的感受。

下面这段代码就展示了运行时如何向一个对象添加属性,一开始我定义了一个对象o,定义完成之后,再添加它的属性b,这样操作是完全没问题的。

    var o = { a: 1 };
    o.b = 2;
    console.log(o.a, o.b); //1 2

为了提高抽象能力,JavaScript的属性被设计成比别的语言更加复杂的形式,它提供了数据属性和访问器属性(getter/setter)两类。

JavaScript对象的两类属性

对JavaScript来说,属性并非只是简单的名称和值,JavaScript用一组特征(attribute)来描述属性(property)。

先来说第一类属性,数据属性。它比较接近于其它语言的属性概念。数据属性具有四个特征。

  • value:就是属性的值。
  • writable:决定属性能否被赋值。
  • enumerable:决定for in能否枚举该属性。
  • configurable:决定该属性能否被删除或者改变特征值。

在大多数情况下,我们只关心数据属性的值即可。

第二类属性是访问器(getter/setter)属性,它也有四个特征。

  • getter:函数或undefined,在取属性值时被调用。
  • setter:函数或undefined,在设置属性值时被调用。
  • enumerable:决定for in能否枚举该属性。
  • configurable:决定该属性能否被删除或者改变特征值。

访问器属性使得属性在读和写时执行代码,它允许使用者在写和读属性时,得到完全不同的值,它可以视为一种函数的语法糖。

我们通常用于定义属性的代码会产生数据属性,其中的writable、enumerable、configurable都默认为true。我们可以使用内置函数 Object.getOwnPropertyDescripter来查看,如以下代码所示:

    var o = { a: 1 };
    o.b = 2;
    //a和b皆为数据属性
    Object.getOwnPropertyDescriptor(o,"a") // {value: 1, writable: true, enumerable: true, configurable: true}
    Object.getOwnPropertyDescriptor(o,"b") // {value: 2, writable: true, enumerable: true, configurable: true}

我们在这里使用了两种语法来定义属性,定义完属性后,我们用JavaScript的API来查看这个属性,我们可以发现,这样定义出来的属性都是数据属性,writeable、enumerable、configurable都是默认值为true。

如果我们要想改变属性的特征,或者定义访问器属性,我们可以使用 Object.defineProperty,示例如下:

    var o = { a: 1 };
    Object.defineProperty(o, "b", {value: 2, writable: false, enumerable: false, configurable: true});
    //a和b都是数据属性,但特征值变化了
    Object.getOwnPropertyDescriptor(o,"a"); // {value: 1, writable: true, enumerable: true, configurable: true}
    Object.getOwnPropertyDescriptor(o,"b"); // {value: 2, writable: false, enumerable: false, configurable: true}
    o.b = 3;
    console.log(o.b); // 2

这里我们使用了Object.defineProperty来定义属性,这样定义属性可以改变属性的writable和enumerable。

我们同样用Object.getOwnPropertyDescriptor来查看,发现确实改变了writable和enumerable特征。因为writable特征为false,所以我们重新对b赋值,b的值不会发生变化。

在创建对象时,也可以使用 get 和 set 关键字来创建访问器属性,代码如下所示:

    var o = { get a() { return 1 } };

    console.log(o.a); // 1

访问器属性跟数据属性不同,每次访问属性都会执行getter或者setter函数。这里我们的getter函数返回了1,所以o.a每次都得到1。

这样,我们就理解了,实际上JavaScript 对象的运行时是一个“属性的集合”,属性以字符串或者Symbol为key,以数据属性特征值或者访问器属性特征值为value。

对象是一个属性的索引结构(索引结构是一类常见的数据结构,我们可以把它理解为一个能够以比较快的速度用key来查找value的字典)。我们以上面的对象o为例,你可以想象一下“a”是key。

{writable:true,value:1,configurable:true,enumerable:true}是value。我们在前面的类型课程中,已经介绍了Symbol类型,能够以Symbol为属性名,这是JavaScript对象的一个特色。

讲到了这里,如果你理解了对象的特征,也就不难理解我开篇提出来的问题。

你甚至可以理解为什么会有“JavaScript不是面向对象”这样的说法了。这是由于JavaScript的对象设计跟目前主流基于类的面向对象差异非常大。

可事实上,这样的对象系统设计虽然特别,但是JavaScript提供了完全运行时的对象系统,这使得它可以模仿多数面向对象编程范式(下一节课我们会给你介绍JavaScript中两种面向对象编程的范式:基于类和基于原型),所以它也是正统的面向对象语言。

JavaScript语言标准也已经明确说明,JavaScript是一门面向对象的语言,我想标准中能这样说,正是因为JavaScript的高度动态性的对象系统。

所以,我们应该在理解其设计思想的基础上充分挖掘它的能力,而不是机械地模仿其它语言。

结语

要想理解JavaScript对象,必须清空我们脑子里“基于类的面向对象”相关的知识,回到人类对对象的朴素认知和面向对象的语言无关基础理论,我们就能够理解JavaScript面向对象设计的思路。

在这篇文章中,我从对象的基本理论出发,和你理清了关于对象的一些基本概念,分析了JavaScript对象的设计思路。接下来又从运行时的角度,介绍了JavaScript对象的具体设计:具有高度动态性的属性集合。

很多人在思考JavaScript对象时,会带着已有的“对象”观来看问题,最后的结果当然就是“剪不断理还乱”了。

在后面的文章中,我会继续带你探索JavaScript对象的一些机制,看JavaScript如何基于这样的动态对象模型设计自己的原型系统,以及你熟悉的函数、类等基础设施。

你还知道哪些面向对象语言,它们的面向对象系统是怎样的?请留言告诉我吧!

国外LEAD广告申请和测试的一些技巧

勤与奋阅读(168)

大家好,很高兴你有能看到我写的文章了,我这里只聊技术和国外LEAD联盟的知识,如果你也在关注这一块知识,刚好可以跟我一起来研究学习和进步,大家都知道我早期是做技术出生的,后来有幸接触到国外LEAD,所以一直就在这条路走到今天。

今天跟大家聊一些关于平时问我的一些问题,就是关于申请广告的时候无情的被广告商拒绝门外,到底是什么原因?如果你多留意是很容易发现问题的,比如你可以自己多去测试,如果你是一个旅游网站,然后去申请一些美食的广告,我相信肯定是不会通过的。

广告商也是需要你的网站内容跟他们相匹配的,如果不匹配你的网站很难让他们相信会带来转换的。所以在选择假站的时候还是需要多留意一下你的广告类型,你想做什么类型的广告就找什么类型的主题去申请,这样成功率会大大的提高。

而找广告我们一般也是有标准的,比如找一些有钱的广告商,如果发一次两次就不发钱了,这样的广告商做起来也累,那么什么样的广告商有钱了?你可以拿到广告商的任务去领英或则facebook上面去找找,看看他们的介绍,也可以去affpaying上面看看介绍。

同时也是可以找一些新的广告商撸的,毕竟新来的广告商也是有一部分很有潜力的,这样技能满足有钱的广告商,又有新的广告商接入,这样也就不怕自己每月断供了。

最后说一下,其实最近很多朋友都在问我关于这行还能做吗?还能赚钱吗?我只能说这一部分都是浮躁,不赚钱为什么我还在做?至少在我认识的圈子里面大家都是在闷着做的,只是想入门没有那么容易了,好了祝大家多多收款。

老薛主机搭建wodpress教程

勤与奋阅读(153)

很多新手不知道怎么搭建网站我这里教大家一下吧。首先打开注册链接,点击这里打开   进入首页如下图

点击虚拟主机,海外主机进入。

这里我们选择优质美国空间,然后选择太阳2号就够我们使用了,然后拉到最下面点击购买。

这里如果注册了域名就把你注册的域名填写进去,然后点击使用进入下个页面。

这个页面简单看一下,没有问题就,就继续,默认所有选项就行了。

这个页面大家一定要记得填写我的优惠码:oxm,然后点击验证,你可以看到158变成118.5了。

优惠过后的金额如下,

 

 

这里我说一下,为什么要选择老薛主机?因为老薛主机支持QQ客服,自己建站不懂的可以联系他们客服进行资讯,而且教程也是最全的,并且他们有免费域名邮箱,很全的中文教程支持,到时候我们去注册联盟最好都用域名邮箱进行注册。关于搭建网站和域名邮箱怎么开通,都可以联系他们客服,有很详细的教程,我之前的文章也有说明,大家可以看看。

比如之前有篇文章介绍速度优化的

如果有任何不懂的关于新建网站的都可以找老薛主机的客服。

开始都使用简单的,等以后大家熟悉后可以使用vps建站。

聊聊做国外网赚需要准备的几个工具

勤与奋阅读(145)

操作国外LEAD网赚启动资金算是比较少的,现在不管做什么项目基本启动资金都是一个门坎,在操作之前还是需要一些操作工具的,这些工具也是我个人长期使用的,虽然后续也更新了部分操作工具,工具他只是解决某个环节的必要工具,重点还是在操作的手法。

主机购买

操作国外联盟通常需要一个网站,网站还需要是全站英文文章,大部分广告商是需要挂的,也就是你拿到广告主的广告代码,然后需要挂在自己网站上面,随着你的操作网站的数量会不断增加,所以选择一个速度快,空间大的主机商是必须的。

我自己最开始使用的是dreamhost主机,一般的操作和使用基本都够用了,注册就会用一个免费的域名,建网站是无限的,并且支持3个月无条件退款,我相信这个条件确实比较实惠,比起国内大部分主机,还是很不错了。

后来我也切换了主机使用了老薛主机,是因为之前用dreamhost主机搭建网站都是独立IP,但是现在支持免费CDN加速,可以隐藏自己的主机IP,所以就果断切换到老薛主机,点击这里注册有优惠哟,也可以填写优惠码:oxm

      域名介绍

域名也是必不可少的工具,我长期使用的是https://www.namesilo.com/ 这个域名,大家注册的时候可以填写优惠码:qinyufen 可以给你1美元直接优惠。

 S5的介绍

S5我最早的时候使用的无忧,现在切换到911S5了,911没有过期的时间,也就是你注册的账号自己购买的S5可以一直放哪里,永久不会过期,并且他们的软件使用也是免费的,功能也很强大。

还给大家推荐一款我用了几年的不错的梯子工具,还比较稳定,大家可以点击注册

 

好了,大家自己慢慢体会吧。