【YDKJS笔记】一、入门与进阶

来源:互联网 发布:106短信平台软件 编辑:程序博客网 时间:2024/05/22 17:01

前言:这本书是受到同学推荐所以找来看,目前感觉真的是没有愧对它那么多的好评,写的非常好,让我感觉跳出了旧的理解到了新世界。这份笔记实际上只是对书本内容的摘录,以便自己之后反复复习。强烈推荐大家一读原书!

一、定义与概念

  1. 字面量:用于表达源代码中一个固定值的表示法(notation),如整数、浮点数、字符串等等。

二、运算符

常见的运算符以及其优先级如下表所示,表格从上到下,优先级从高到低

操作符类型 操作符 成员运算符 . [] 调用/创建实例 () new 否定/自增 ! ~ -(负) +(正) ++ – typeof void delete 乘/除
    / %
加/减
    -
位移操作符 << >> >>> 关系操作符 < <= > >= in instanceof 等于 == != === !== 按位与 & 按位异或 ^ 按位或 | 逻辑与 && 逻辑或 || 条件运算 ?: 赋值 = += -= *= /= %= <<= >>= >>>= &= ^= |= 逗号 ,

tips:
1. == 与 === ?
==是宽松等价,会自动转换类型再判断是否相等,即检查值的等价性;
===是严格等价,不会自动转换类型,即检查值和类型两者的等价性。
使用==和===的规则如下:
(1) 如果比较的两个值之一是true/false值,避免==而使用===;
(2) 如果比较的两个值之一是0, “”,或[](空数组),避免==而使用===;
(3) 在所有其他情况下,使用==是安全的,并且在很多情况下可以简化代码并改善可读性;
注意:
当比较两个非基本类型值,如object(包含function和array),那么==和===比较都会简单的检查这个引用是否相同,而不是它们底层的值
例如array默认情况下会使用逗号(,)连接所有值来强制转换为string,此时用==来判断情况如下:

var a = [1,2,3];var b = [1,2,3];var c = "1,2,3";a == c;          // trueb == c;          // truea == b;          // false,比较其引用并不一致

2. 没有“严格不等价”可用!!
当比较的两个值都是string,那么两个值会以字典顺序进行比较;当两个值之一不是string,那么两个值就将被强制转换为number(当字符串不为数字时会被转化为NaN),并进行一般的数字比较,举例如下:

var a = "41";var b = "42";var c = "43";var d = "foo";a < b;        // trueb < c;        // truea < d;        // falsea > d;        // falsea == d;       // false

3. <<、>> 与 >>> ?
<<:是左移运算符,num << 1,相当于num乘以2;
>>:右移运算符,num >> 1,相当于num除以2;
>>>:无符号右移,忽略符号位,空位都以0补齐。

三、变量

  1. 静态类型语言:在某些编程语言中,可以声明一个变量(或容器)来持有特定类型的值,比如C语言中声明int,表示为整数类型,这就是静态类型,也称为类型强制。这种语言的优点是防止了意外的类型转换,有利于程序的正确性
  2. 动态类型语言:某些编程语言不在变量上强调类型,而是在值上,这就是动态类型,也称为弱类型,允许变量在任意时刻持有任意类型的值。这种语言的好处是允许一个变量在程序逻辑流程中代表一个值而不论这个值在任意给定时刻是什么类型,有利于程序的灵活性。JavaScript就是一种动态语言。
  3. 变量通常使用var进行声明,这种声明没有其他类型的信息:var a=1;
  4. 常量可以声明为const:const TAX_RATE = 3.14; 依据惯例,用作常量的JavaScript变量通常是大写的,在多个单词之间使用下划线_连接。const防止在初始设置之后对常量的意外修改。若对其进行修改会出现错误。

四、作用域

作用域基本上就是变量的集合,也就是如何使用名称访问这些变量的规则,只有在同一个作用域内才能够访问。一个作用域可以嵌套在另一个中,这样在内部作用域中的代码就可以访问这两个作用域中的变量。

五、值与类型

内建类型

JavaScript拥有带类型的值,没有带类型的变量。下面是可用的内建类型:
1. string
2. number
3. boolean
4. nulltypeof null会错误的返回“object”。这是JavaScript一直存在的一个bug,但是有太多的代码依托于这个bug,因此修复这个bug会导致更多的bug。
5. undefined:一个变量可以用好几种不同的方式得到“undefined”值的状态,包含未被复制的变量、直接定义为undefined的变量、没有返回值的函数和使用void操作符。
6. object:一种复合值,可以在它上面设定属性(带名称的位置),每个属性持有各自的任意类型的值。
属性既可以使用点号标记法(如obj.a)访问,也可以使用方括号标记法(如obj[“a”])访问,一般情况下更加推荐点号标记法,更加易于阅读。
7. symbol (ES6新增类型):ES5对象属性名都是字符串容易造成属性名的冲突,导致属性被重写。symbol集中的值可以由程序创建并作为属性的键来使用,也不用担心名称冲突。symbol变量从创建开始就是不可变的,不能为之设定属性,它可以作为属性名(类字符串性质)。其值必须通过方括号获取。注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。
symbol的弱封装机制:一个模块可以创建几个symbols在对象中任意使用而不用担心与其他模块的属性冲突。

JavaScript可以通过一个typeof操作符的值变量的类型。注意形如”typeof a”并并不是在询问“a的类型”,而是“当前a中的值的类型”,在JavaScript中只有值拥有类型,变量只是这些值的简单容器。
原始类型(primitive type)有以下五种类型:Undefined,Null,Boolean,Number,String。

子类型

JavaScript程序中有另外两种常见的值类型:数组和函数。这些类型更像是object类型的特化版本,也可以说是子类型。
数组:一个数组是一个object,它不使用特殊的带名称的属性/键持有值(任意类型),而是使用数字索引,比如

var arr = [    "hello world",    42,    true];

函数:typeof返回“function”这暗示“function”是一种主要类型,因此也可以拥有属性,但是函数对象属性仅在有限情况下才会被使用。

六、函数作用域和提升(hoisting)机制

使用var关键字声明的变量将属于当前的函数作用域,如果声明位于任何函数外部的顶层,该变量就属于全局作用域。

提升机制

无论var出现在一个作用域内部的何处,这个声明都被认为是属于整个作用域,而且在作用域的所有位置都是可以访问的,这种行为称为提升。比喻一个var声明在概念上被移动到了包含它的作用域的顶端。
考虑如下代码:

var a = 2;foo();              // 可以工作,因为`foo()`声明被“提升”了function foo() {    a = 3;    console.log(a); // a=3;    var a;          // 声明被“提升”到了`foo()`的顶端}console.log(a);     // a=2;

注意:在一个作用域中依靠变量提升来在var声明出现之前使用一个变量不是一个好的行为,这会使得代码易读性降低。而使用被提升的函数声明要常见得多,也更被接受,正如我们在foo()正式声明之前就调用它。

嵌套的作用域

当一个变量被声明时,它就在这个作用域内的任何地方都是可用的,包括任何下层/内部作用域。
当我们试图在一个作用域内访问一个不可用的变量值时,程序会抛出RefferenceError。若开发者试着为一个还没有被声明的变量赋值,那么根据“strict模式”的状态,开发者要么得到一个在顶层全局作用域中创建的变量(不推荐!),要么得到一个错误。考虑如下代码:

function foo() {    a = 1; // `a`没有被正式声明}foo();a;         // 1 -- 自动全局变量 

上述做法非常不合理,应该总是给变量进行正式声明。

let关键字(ES6)

除了在函数级别为变量创建声明,ES6给出了let关键字声明变量属于个别块(一个{ ... })的变量。其作用域规则大致上与以上看到的函数相同:

function foo() {    var a=1;    if(a>=1) {        let b=2;        while(b<5) {            let c=b*2;            b++;            console.log(a+c);        }    }}foo();   // 5 7 9

使用let声明的b将仅属于if语句,而不是整个foo()函数的作用域,类似的c仅属于while循环。
let关键字提供的块作用域,使得开发者可以以更加细粒度的方式管理变量作用域,这样使得代码随着时间的推移更加易于维护。

Strict模式(ES5)

strict模式是ES5新加入的,它收紧了一些特定行为的规则。一般来说这些限制被视为使代码符合一组更安全和更合理的指导方针。坚持strict模式一般会使代码对引擎有更强的可优化性
根据摆放stric模式注解的位置,可以为一个单独的函数或者一整个文件切换到strict模式:

// 对单独的一个函数function foo() {    "use strict";    // 这部分代码是strict模式的    function bar() {        // 这部分代码是strict模式的    }}// 这部分代码不是strict模式的

与之对比如下:

// 对整个文件"use strict";function foo() {    // 这部分代码是strict模式的    function bar() {        // 这部分代码是strict模式的    }}// 这部分代码是strict模式的

使用strict模式的一个关键改善是不允许因为省略了var而进行隐含的自动全局变量声明

七、立即被调用的函数表达式(IIFE)

一个立即调用的函数表达式(IIFE)可以用于执行函数表达式:

(function IIFE() {    console。log("Hello!");})(); // "Hello!"

实际上,围绕在函数表达式(function IIFE(){ .. })外部的(..)只是一个微妙的JS语法,用于防止函数表达式被看作一个普通的函数说明。
在表达式末尾的最后的()才是实际立即执行它前面的函数表达式的东西。考虑foo()和IIFE之间的相似性如下:

function foo(){ .. }// `foo`是函数引用表达式,然后用`()`执行它foo();// `IIFE`是函数表达式,然后用`()`执行它(function IIFE(){ .. })();

(function IIFE(){ .. })与定义的foo实质上是相同的,在这两种情况下,函数引用都使用立即在它后面的()执行。
因为IIFE只是一个函数,而函数可以创建变量作用域,所以IIFE经常用于定义变量,这些变量不会影响围绕在IIFE之外的代码:

var a = 42;(function IIFE(){    var a=10;    console.log(a); // 10})();console.log(a); // 42// IIFE还可以有返回值var x = (function IIFE(){    return 42;})();console.log(x); // 42

八、闭包

闭包是这样一种方法:即使函数已经完成了运行,它依然可以“记住”并持续访问函数的作用域。
考虑如下代码:

function makeAdder(x) {    // 参数`x`是一个内部变量       // 内部函数`add()`使用`x`,所以它对`x`拥有一个闭包       function add(y) {        return y+x;    };    return add;}

每次调用外部的makeAdder(..)所返回的对内部add(..)函数的引用可以记住被传入makeAdder(..)的x值。现在,让我们使用makeAdder(..)

// `plusOne`得到一个指向内部函数`add(..)`的引用,// `add()`函数拥有对外部`makeAdder(..)`的参数`x`的闭包var plusOne = makeAdder(1);var plusTen = makeAdder(10);plusOne(3); // 4<--1+3plusOne(41); // 42<--1+41plusTen(13); // 23<--10+13

这段代码的工作方式是:
当我们调用makeAdder(1)时,我们得到一个指向它内部的add(..)的引用·,他记住了x是1,这个函数引用被称为plusOne(..)
当我们调用plusOne(3)时,它在3(它内部的y)上加1(被x记住的),于是得到结果4。

九、模块

在JavaScript中闭包最常见的用法就是模块模式。模块让你定义对外面世界不可见的私有实现细节(变量,函数),和对外面可访问的公有API。

function User() {    var username, password;    function doLogin(user, pw) {        username = user;        password = pw;        // 做登录的工作    }    var publicAPI = {        login: doLogin    };    return publicAPI;}// 创建一个`User`模块的示例var fred = User();fred.login("fred", "12Battery34!");

警告:此处没有调用new User()User()只是一个函数,不是一个要被初始化的对象,所以它只是被一般地调用,使用new不合适,而且浪费资源。
执行User()创建了User模块的一个实例——一个全新的作用域会被创建,而每个内部变量/函数的一个全新的拷贝也因此而被创建。
内部的doLogin()函数在username和password上拥有闭包,这意味着即便User()函数已经完成了运行,它依然持有对它们的访问权。
publicAPI是一个带有属性/方法的对象,login是一个指向内部doLogin()函数的引用,当我们从User()中返回publicAPI时,它就成为了我们称为fred的实例。
此时虽然外部的User()函数已经完成执行,但是usernamepassword这些内部变量由于login函数里的闭包的存在,仍然存活。

十、this标识符

如果一个函数的内部有一个this引用,但是指向哪一个object要看函数是如何被调用的。
这是一个快速的说明:

function foo(){    console.log(this.bar);}var bar = "global";var obj1 = {    bar: "obj1";    foo: foo};var obj2 = {    bar: "obj2"};foo();          // "global"obj1.foo();     // "obj1"foo.call(obj2); // "obj2"new foo();      // "undefined"

关于this如何被设置有四个规则,如上述:
1. foo()最终在非strict模式中将this设置为全局对象,在strict模式中,this将会是undefined而且在访问bar属性时得到一个错误。所以this.bar的值是global
2. obj1.foo()将this设置为对象obj1
3. foo.call(obj2)this设置为对象obj2
4. new foo()将this设置为一个新的空对象。

十一、原型

当引用一个对象的属性时,若属性不存在,JavaScript会自动地使用这个对象的内部原型引用来寻找另外一个对象,在上面查询想要的属性。
从一个对象到它备用对象的内部原型引用链接发生在这个对象被创建的时候。说明原型最简单的方法就是使用内建工具Object.create(..)
考虑如下代码:

var foo = {    a: 42};// 创建`bar`并将它连接到`foo`var bar = Object.create(foo);bar.b = "hello world";bar.b;  // "hello world"bar.a;  // 42 <-- 委托到`foo`

使用原型的更自然的方式是一种称为“行为委托”的模式,在这种模式中有意地将被链接的对象设计为可以从一个委托到另一个的部分所需的行为中。

十二、旧的与新的

将新的JavaScript特性“带到”老版本的浏览器中有两种方法:填补和转译。
1. 填补(Polyfilling)
拿来一个新特性的定义并制造一段行为等价的代码,但是这段代码可以运行在老版本的JS环境中。
例如,ES6定义了一个称为Number.isNaN(..)的工具来检查NaN值,同时废弃原来的isNaN(..),使用填补的方法如下:

if(!Number.isNaN) {    Number.isNaN = function isNaN(x) {        return x !== x;    };}

填补实现应当非常小心,使用信任的,经受过检验的填补,比如由ES5-Shim提供的填补往往更好。
2. 转译(transpiling)
没有任何办法可以填补语言中新增加的语法。在老版本的JS引擎中新的语法将因为不可识别/不合法而抛出一个错误。所以更好的选择是使用一个工具将新版本代码转换为等价的老版本代码。这个处理通常称为“转译”,表示转换+编译。
既然要转译成老版本的代码,为什么不直接编写老版本代码呢?原因是多方面的:

  • 在语言中新加入的语法是为了使代码更具有可读性和维护性而设计的。老版本的等价物往往会绕多得多的圈子。所以应当首选编写新的和干净的语法。
  • 为最新的浏览器提供新语法,我们可以利用浏览器对新语法进行的性能优化。这也让浏览器制造商有更多真实世界的代码来测试它们的实现和优化方法。
  • 提早使用新语法可以允许它在真实世界中被测试得更加强壮,给JavaScript委员会(TC39)提供更早的反馈。使得问题能够尽早发现,使得语言设计错误在变得无法挽回之前改变/修改它。
    以下给出一个简单例子,ES6增加了一个被称为“默认参数值”的新特性:

    function foo(a=2) {    console.log(a);}foo();   // 2foo(42); // 42

    而在老版本环境中需要转译器来对这段代码进行转译:

    function foo() {    var a = arguments[0] !== (void 0)? arguments[0] : 2;    console.log(a);}

    转译器检查arguments[0]值是否是void 0(也就是undefined),如果是就提供默认值2
    除了可以现在就在老版本浏览器中使用更好的语法外,观察转译后的代码实际上更清晰的解释了意图中的行为。
    转译后的代码清楚的展示undefined是唯一不能作为参数默认值的明确传递的值,但是转译后的代码使这一点清楚得多。

十三、非JavaScript

现实中大多数JS程序都是在浏览器这样的环境中运行并与之互动的,现在很大一部分的代码,严格来说,不是直接由JavaScript控制的。
比如最常见的
DOM API:var e1 = document.getELementById("foo");
输入输出(I/O),如alert()

参考

[1] Kyle Simpson. You Don\’t Know JS

原创粉丝点击