Web瞎捣鼓——JavaScript初章

和自甴很熟 提交于 2020-08-19 19:32:27

“瞎捣鼓”系列写了两篇感觉好累,就是因为觉得“前端之旅”写得不好才重写,写完之后还是觉得不好。在MDN上看到一句话:学习编程,语法本身并不难,真正困难的是如何应用它来解决现实世界的问题。我想之所以还是认为写得不好,大抵在于我想要解决问题又没想好解决什么问题的缘故。

老规矩,先来一段历史。

1994年,Netscape发布Netscape Navigator,很快他们发现服务器带宽和资费不利于在服务器进行某些操作。比如数据未校验就直接提交服务器,服务器发现后再告诉用户,他们需要在浏览器就将已知错误告诉用户。

1995年,Netscape聘用Brendan Eich来开发一种语言以实现该目的,Brendan Eich只用了10天就完成了任务。小名叫Mocha,大名LiveScript,随后Netscape借 Sun公司Java之名与Sun联合发布该语言,艺名JavaScript。

1996年,Navigator 2.0置入JavaScript,这个时候Microsoft公司看到了好处,于是照方抓药克隆了一套取名“JScript”。这对Netscape而言完全没法竞争,自己弄出来的东西突然没了话语权,一生气就将JavaScript交给了国际化标准组织ECMA。另外,除了这两家,还有一家基于CEnvi的ScriptEase混在这场角斗中。当然Microsoft才是大家,也与JavaScript同方向。于是,ECMA组织把各大厂商召集起来开会研究,最终得到新的脚本语言ECMAScript。

1997年~1999年,相继发布ECMAScript 1.0~3.0发布成为通行标准。这只是一个标准,大方向,各派系底下各自又有各自的小算盘。

2007年~2009年,ECMAScript 4.0出现争议最后废除,4.0少部分功能作为3.1开发,代号Harmony。不久后3.1定为5.0,后成为ISO国际标准。在各大厂商争执不下的情况下,Harmony一分为二:可行的想法定名为JavaScript.next开发,然后演变成ECMAScript 6;不成熟视为JavaScript.next.next,在更远的将来考虑。

2013年,ECMAScript 6草案被冻结,新功能放到ECMAScript 7,有点历史重演的节奏。12月,ECMAScript 6草案发布,听取各方意见。

2015年,ECMAScript 6正式发布,更名ECMAScript 2015,以后每年发布一个版本以年份作为版本号。

这和CSS 3.0有点像,新的功能在成熟后逐步同步为标准,所以我觉得它们都应该算是ECMAScript 6,后文用ES6代指。

阅读发展史对学习这门语言虽然没啥好处,但是以史为鉴还是有必要的。从HTML到CSS再到JavaScript,它们一步一步的发现技术问题然后解决。从历史的角度上看,做程序员也需要具有前瞻的目光,特别是在这瞬息万变的时代。先进的技术就是先进的生产力,等它真的发展起来再关注,也就只能等着喝稀饭了。我认为,为企业服务无非两种人比较容易混:一种是身上有企业需要且别人无法替代价值,另一种是初出茅庐有极大潜力。前者创造价值,后者物美价廉。

JavaScript是由ECMAScript、DOM、BOM共同组成。ECMAScript标准由ECMA组织制定,DOM由W3C组织制定,BOM好像没有组织。

变量

几乎每个语言都有数据类型、变量声明、运算符、表达式等基本语法。讲道理ECMAScript有的东西JavaScript应该都有,不讲道理的是浏览器厂商。JavaScript是一种具有函数优先的轻量级、解释型或即时编译型的弱类型编程语言,它的语法来自ECMAScript。

JavaScript的变量是动态类型,由赋值时的数据决定,使用var、let、canst进行变量声明。var声明的变量的特点:语句块中声明的变量将成为语句块所在函数或全局作用域的局部变量;可以先使用变量后声明而不会引发异常;可以重复声明(这里的前提是非严格模式)。

console.log(a);  // undefined

var a = [];

for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}

a[6]();  // 10

var a = 'x';

console.log(a);  // x

将代码中的var改成let:未声明使用会报错,重复声明会报错,for循环的值达到预期。let有块级作用域,会出现暂时性死区。这不用多讲,关于ES6语法可参看阮一峰的《ECMAScript 6 入门》,另外在前面“前端之旅”系列中也有涉及。

ES6另外一个关键字const,const声明一个只读的常量,一旦声明就不能改变。但不能说是完全控制,引用一段阮一峰的话

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。 

换种表述方式就是:简单类型的数据就好比申请一块地建学校,这块地就必须用来建学校。复合类型的数据就好比只是申请了一块地,只要合法至于是修酒店、修住宅还是干啥,修完才知道。

const bar = 1;  // const分配一块内存给bar
bar.prop = 'a';  // 不会报错
console.log(bar);  // 1

const foo = {};  // const分配一块内存给foo
foo.prop = 'a';  // 不会报错
console.log(foo);  // 修改成功,{foo:'a'}
foo = {};  // TypeError

下面我的个人一些理解:从JavaScript的数据类型上看,它的数据类型本质上都是object。用强类型语言的角度看,它不是基本数据类型,而是一种数据结构或者数据泛型。此外,JavaScript变量的数据类型是动态的,就是说内存动态分配,那么let/var声明的变量是指针,const声明的变量也就是常指针。(ps:这些知识很多年没用,仅供参考,大神路过请多多指教)

const type = (data) => Object.prototype.toString.call(data);

const isTrue = true;
console.log(type(isTrue));  // [object Boolean]
console.log(type(Boolean));  // [object Function]

const str = '123';
console.log(type(str));  // [object String]
console.log(type(String));  // [object Function]

当然,那只是我的个人理解方式,并不能说明JS解释器不能根据数据类型进行正确的内存分配。

关于基本数据类型,ECMAScript标准定义了七种:Boolean、Null、Undefined、Number、BigInt、String、Symbol。除七种基本类型外,还有Object。MDN并没有将Aarry和Function作为数据类型。

null和undefined都表示空值,那么他们的区别是什么?

 console.log(null == undefined);  //true
 console.log(null === undefined);  //false
  • 它们是不同的数据类型。
  • null表示对象为空,undefined表示缺少值。

对象

对象是一个包含属性和方法的集合,分为内建对象(Math、String等)、宿主对象(BOM、DOM)和自定义对象。自定义对象可以通过直接通过变量进行创建,可以通过点方法和括号表示法添加(访问也是用这两种方式)。

let obj = {
  key: 'value',
  method: function () {
    console.log('method')
  }
};
console.log(obj.key);  // value
obj.method();  // method

obj.newkey = 'new value';
obj['newmethod'] = function () {
  console.log('new method');
}
console.log(obj['newkey']);  // new value
obj['newmethod']();  // new method

对象的属性有两种类型:数据属性和访问器属性。要修改属性特性,必须使用Object.defineProperty()方法。

数据属性包含一个数据值的位置,有四个描述其行为的特性:

  • [configurable]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
  • [enumerable]:表示能否通过for-in循环返回属性。
  • [writable]:表示能否修改属性的值。
  • [value]:包含这个属性的数据值。

访问器属性不包含数据属性的[writable]和[value],另外增加Getter和Setter函数:

  • [get]: 读取属性时调用的函数。

{get prop() { ... } }
{get [expression]() { ... } }

  • [set]: 写入属性时调用的函数。

{set prop(val) { . . . }}
{set [expression](val) { . . . }}

JavaScript内有许多内置标准对象,可以在支持添加新属性的任何标准内置对象或自定义对象内定义getter和setter。在ES6中,有更简洁定义方法的语法:

var obj = {
  // ES5
  property( parameters… ) {},
  *generator( parameters… ) {},
  async property( parameters… ) {},
  async* generator( parameters… ) {},

  // ES6
  [property]( parameters… ) {},
  *[generator]( parameters… ) {},
  async [property]( parameters… ) {},

  // Getter Setter
  get property() {},
  set property(value) {}
}

在JavaScript中,一切皆对象。关于这一点的理解,可以参考我很早之前写的《前端之旅——JavaScript篇》,里面详细描述了对象的原型和原型链,我觉得写得还不错。还有一些其他关于JavaScript的重要概念,包括闭包、封装和继承等,本文将一笔带过这些内容。

这里挂一张从别人那儿拿来的原型链的说明图(传送),另外还有一篇写得很详尽的文章(传送)。

函数

对象和函数是ECMAScript的两个基本元素,对象相当于存放值的一个命名容器,函数相遇程序能够执行的步骤。定义函数有多种方法:

函数声明:

function name([param[, param[, ... param]]]) { statements }

函数表达式:

var myFunction = function name([param[, param[, ... param]]]) { statements }

Generator函数:

function* name([param[, param[, ...param]]]) { statements }

Generator函数表达式:

function* [name]([param] [, param] [..., param]) { statements }

AsyncFunction函数表达式(MDN没有放入):

async function [name]([param] [, param] [..., param]) { statements }

箭头函数表达式:

([param] [, param]) => { statements } param => expression

Function构造函数(不推荐):

new Function (arg1, arg2, ... argN, functionBody)

Generator构造函数:

new GeneratorFunction (arg1, arg2, ... argN, functionBody)

AsyncFunction构造函数(MDN没有放入):

new AsyncFunction([arg1[, arg2[, ...argN]],] functionBody)

注意:GeneratorFunction和AsyncFunction并不是一个全局对象,需要通过方法来获取:

Object.getPrototypeOf(function*(){}).constructor

Object.getPrototypeOf(async function(){}).constructor

构造函数、函数声明和函数表达式所起作用差不多,有一些细微的差别(整理自MDN):

  • 函数名和函数的变量存在着差别。函数名不能被改变,但函数的变量却能够被再分配。函数名只能在函数体内使用。
  • 被函数赋值的变量仅仅受限于它的作用域,该作用域确保包含着该函数被声明时的作用域。
  • 使用用 'new Function'定义的函数没有函数名。
  • 和通过函数表达式定义或者通过Function构造函数定义的函数不同,函数声明定义的函数可以在它被声明之前使用。
  • 函数表达式定义的函数继承了当前的作用域。换言之,函数构成了闭包。另一方面,Function构造函数定义的函数不继承任何全局作用域以外的作用域(那些所有函数都继承的)。
  • 通过函数表达式定义的函数和通过函数声明定义的函数只会被解析一次,而Function构造函数定义的函数却不同。
  • 在通过解析Function构造函数字符串产生的函数里,内嵌的函数表达式和函数声明不会被重复解析。
var foo = (new Function("var bar = \'FOO!\';\nreturn(function() {\n\talert(bar);\n});"))();
foo(); // 函数体字符串"function() {\n\talert(bar);\n}"的这一部分不会被重复解析。

当函数只使用一次时,通常使用立即调用表达式(IIFE),它是在函数声明后立即调用。

(function() {
    statements
})();

前文提到一个重要的概念——闭包

函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包(closure)。也就是说,闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,每当函数被创建,就会在函数生成时生成闭包。

前面很好理解,它的意思大概可以理解为定义在一个函数内部的函数。最后一句很难理解,在网上找到一种解释说:因为它们都是对象都关联到了作用域链(传送)。

最后

这里所写的大部分内容只是JavaScript的一角,ECMAScript语法的一小部分,它的标准内置对象也只是略过。从大的说,它确实没法讲:有的太大没法拉开,比如WebAssemly、Intl;有的一拉开就会牵扯很多东西,比如之前前面提到的Generator;有的看的就是实际应用,比如ArrayBuffer;还有的写出来就是一串概念,比如Math……

当然,API这种东西不结合实际就显得就是抄书,所以DOM、BOM不可能展开巴拉巴拉,不实际。我不太适合写一写概念性很强的东西,有时也不得不去理这些理论。正如在前面文章所说,当前的文章只是一个宏观的轮廓介绍,在未来会陆陆续续完善这些文章。我现在所做就是一个系统化的过程,可能也会罗里吧嗦的提到一些点,以后会逐步进入实战并回归细化。

突然想到Date,以前写获取每月最后一天:先获取目标月的下一个月的第一天,然后减去24小时。有一天我就查怎样获取最后一天,看到有人给Date构造传入目标年月,日期参数传0,就是最后一天。然后我就想这个合理吗?最后在MDN上看到:

如果 dayValue 超出了月份的合理范围,setDate 将会相应地更新 Date 对象。

例如,如果为 dayValue 指定0,那么日期就会被设置为上个月的最后一天。

如果dayValue被设置为负数,日期会设置为上个月最后一天往前数这个负数绝对值天数后的日期。-1会设置为上月最后一天的前一天。

我觉得我更适合不断去冒险。

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!