JS学习

学习过程:

promise—>箭头函数—>作用域—>闭包

js基础

入门篇

基本语法

变量提升

JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。其中,函数名同样被视为变量名,所以一样会被提升到代码头部。

标识符

  • 不能以数字开头,可以以 _$ 开头
  • JavaScript 有一些保留字,不能用作标识符:arguments、break、case、catch、class、const、continue、debugger、default、delete、do、else、enum、eval、export、extends、false、finally、for、function、if、implements、import、in、instanceof、interface、let、new、null、package、private、protected、public、return、static、super、switch、this、throw、true、try、typeof、var、void、while、with、yield。

标签

js允许语句前面可以带有标签,方便跳出块区域和多重循环,常与for循环、continue、break连用。

数据类型

##null,布尔值

boolean

如果 JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false,其他值都视为true。

  • undefined
  • null
  • false
  • 0
  • NaN
  • “”或’’(空字符串)

注意,{}[]都是true。

数值

  • js中所有数都是小数,64位,其中符号位1位,表示指数的11位,剩余的52位表示精度,但是由于表示精度的最高位1.xxxx中的1被省略,所以可以表示53位精度;
  • parseInt即将字符串转换为整数型,不管是不是字符串,都会先转换为字符串,再转为整数型,其中,parseInt可以传入两个参数,第二个参数可以是转换进制值;

字符串

  • 想输出多行字符串,有一种利用多行注释的变通方法

    1
    2
    3
    4
    5
    6
    7
    8
    (function () { /*
    line 1
    line 2
    line 3
    */}).toString().split('\n').slice(1, -1).join('\n')
    // "line 1
    // line 2
    // line 3"
  • Base64转码

    • Base64 就是一种编码方法,可以将任意值转成 0~9、A~Z、a-z、+和/这64个字符组成的可打印字符。使用它的主要目的,不是为了加密,而是为了不出现特殊字符,简化程序的处理。
    • JavaScript 原生提供两个 Base64 相关的方法。

      • btoa():任意值转为 Base64 编码
      • atob():Base64 编码转为原来的值
      1
      2
      3
      var string = 'Hello World!';
      btoa(string) // "SGVsbG8gV29ybGQh"
      atob('SGVsbG8gV29ybGQh') // "Hello World!"
* 要将非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节,再使用这两个方法。

1
2
3
4
5
6
7
8
9
10
function b64Encode(str) {
return btoa(encodeURIComponent(str));
}

function b64Decode(str) {
return decodeURIComponent(atob(str));
}

b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE"
b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好"

对象

  • 对象的引用
    两个变量同时指向同一个对象,其中任何一个变量添加属性,另一个变量都可以读写该属性,但是如果取消某个变量对于原对象的引用,并不会影响到另一个变量;
    注意:仅限于对象,如果两个变量同时指向同一个原始类型的值,那么变量此时都是值的拷贝而已;
  • 如果行首是一个大括号,js引擎一律将其解释为代码块,如果想要表示为对象,则必须在大括号前加上一个圆括号,这种差异在eval语句(作用是对字符串求值)中体现的尤为明显;

    1
    2
    eval('{foo: 123}') // 123
    eval('({foo: 123})') // {foo: 123}
  • 属性读取:一种使用点运算符,另外一种采用方括号运算符,注意若使用方括号运算符,键名必须放在引号里面,否则会被当做变量处理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var foo = 'bar';

    var obj = {
    foo: 1,
    bar: 2
    };

    obj.foo // 1
    obj[foo] // 2
  • 查看一个对象本身所有属性,可以使用Object.keys方法

    1
    2
    3
    4
    5
    6
    7
    var obj = {
    key1: 1,
    key2: 2
    };

    Object.keys(obj);
    // ['key1', 'key2']
  • 属性的遍历:for…in 循环:

    • 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。
    • 它不仅遍历对象自身的属性,还遍历继承的属性。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      var obj = {a: 1, b: 2, c: 3};

      for (var i in obj) {
      console.log('键名:', i);
      console.log('键值:', obj[i]);
      }
      // 键名: a
      // 键值: 1
      // 键名: b
      // 键值: 2
      // 键名: c
      // 键值: 3
  • with语句:作用是操作同一个对象的多个属性时,提供方便,但是不建议使用,因为在with语句里面没有改变作用域,导致绑定对象不明确,如果with区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量。

    1
    2
    3
    4
    5
    6
    7
    8
    var obj = {};
    with (obj) {
    p1 = 4;
    p2 = 5;
    }

    obj.p1 // undefined
    p1 // 4

函数

函数声明

  • function命令

    1
    2
    3
    function print(s){
    console.log(s);
    }
  • 函数表达式(如果function后带函数名,函数名也只在函数体内部有效)

    1
    var print = function(){console.log(s)};
  • Function构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var add = new Function(
    'x',
    'y',
    'return x + y'
    );

    // 等同于
    function add(x, y) {
    return x + y;
    }

函数ToString

可以解析注释实现多行字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
var multiline = function (fn) {
var arr = fn.toString().split('\n');
return arr.slice(1, arr.length - 1).join('\n');
};

function f() {/*
这是一个
多行注释
*/}

multiline(f);
// " 这是一个
// 多行注释"

函数作用域

在ES6中,有三种作用域:全局作用域、块作用域、函数作用域。
函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关。

1
2
3
4
5
6
7
8
9
10
11
var a = 1;
var x = function () {
console.log(a);
};

function f() {
var a = 2;
x();
}

f() // 1

上面代码中,函数x是在函数f的外部声明的,所以它的作用域绑定外层,内部变量a不会到函数f体内取值,所以输出1,而不是2。

总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。
所以说,如果函数A调用函数B,函数B不会引用函数A的内容。

参数传值

  • 函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递;
  • 函数参数如果是复合类型的值(数组、对象、其他函数),传递方式是传址传递;
  • 函数内部修改的如果是参数对象的某个属性,那么会影响到原始值,如果是替换掉整个参数,则不会影响到原始值:

    1
    2
    3
    4
    5
    6
    7
    8
    var obj = [1, 2, 3];

    function f(o) {
    o = [2, 3, 4];
    }
    f(obj);

    obj // [1, 2, 3]

arguments

  • arguments对象包含了函数运行时的所有参数,但是它并不是一个数组,而是一个对象,所以它不能用数组中的slice和forEach方法;
  • 在正常模式下,arguments对象可以在运行时修改,但是在严格模式下,修改arguments对象的值并不会改变真实参数的值;
  • 如果要让arguments对象使用数组方法,真正的解决办法就是让arguments对象变为数组,常用的转换方法有两种,分别是slice方法和逐一填入新数组:

    1
    2
    3
    4
    5
    6
    7
    var args = Array.prototype.slice.call(arguments);

    // 或者
    var args = [];
    for (var i = 0; i < arguments.length; i++) {
    args.push(arguments[i]);
    }

闭包

闭包,简而言之就是函数中的函数,闭包最大的特点,就是它可以“记住”诞生的环境,用处有三个:

1. 可以读取函数内部的变量;
2. 让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在;
3. 封装对象的私有属性和私有方法。
1
2
3
4
5
6
7
8
9
10
11
function createIncrementor(start) {
return function () {
return start++;
};
}

var inc = createIncrementor(5);

inc() // 5
inc() // 6
inc() // 7
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() {
return _age;
}

return {
name: name,
getAge: getAge,
setAge: setAge
};
}

var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25

“立即调用的函数表达式”(Immediately-Invoked Function Expression)

1
2
3
(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。

eval

  • 本质是在当前域中注入代码,可以接受一个字符串作为参数,并且将该字符串当做语句执行,如果识别不是字符串,则会原样返回。
  • 同时,eval没有自己的作用域,会改变当前域原有变量的值,当然在严格模式下,eval内部自己声明的变量有自己的作用域,不会影响到外部作用域的值。
  • 由于引擎无法分辨eval的别名调用,所以在eval使用别名时,一律都是全局作用域。

数组

数组遍历

  • 数组遍历可以考虑使用for循环、while循环、forEach循环,会返回键值(键名为整数),for…in…循环会同时遍历非整数键(返回的是键名);
  • 数组遍历空位时,需要格外注意,空位和undefined是不一样的,空位表示数组中没有这个元素,所以在for…in…和forEach循环中不会被遍历到,但是undefined代表数组有这个元素,是会遍历到的;

运算符

算术运算符

加法运算符

  • 对象的相加
    如果运算子是对象,必须先转成原始类型的值,引擎会自动调用obj.valueof().toString(),所以我们可以重新valueOf()和toString()方法即可,其中有个特例,当运算子是一个Date对象的实例,那么会优先执行toString方法。
  • 指数运算符
    ** 为指数运算符,是右结合的,当然,三元条件运算符也是右结合的,当然还有赋值运算符(=)也是右结合的。

比较运算符

  • 严格相等运算符
    JavaScript 提供两种相等运算符:== 和 === 。
    简单说,它们的区别是相等运算符(==)比较两个值是否相等,严格相等运算符(===)比较它们是否为“同一个值”。如果两个值不是同一类型,严格相等运算符(===)直接返回false,而相等运算符(==)会将它们转换成同一个类型,再用严格相等运算符进行比较。
    tip: undefined和null与自身严格相等

    布尔运算符

    二进制位运算符

其他运算符,运算顺序

void 运算符

void运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined,主要用途是浏览器的书签工具,以及在超链接中插入代码防止网页跳转。

语法专题

错误处理机制

原生错误类型

  • SyntaxError
  • ReferenceError
  • RangeError
  • TypeError
  • URIError
  • EvalError

自定义错误

除了 JavaScript 原生提供的七种错误对象,还可以定义自己的错误对象

1
2
3
4
5
6
7
function UserError(message) {
this.message = message || '默认信息';
this.name = 'UserError';
}

UserError.prototype = new Error();
UserError.prototype.constructor = UserError;

上面代码自定义一个错误对象UserError,让它继承Error对象。然后,就可以生成这种自定义类型的错误了

1
new UserError('这是自定义的错误!');

标准库

Object

四个用法:

  • Object本身就是一个函数,可以将任意值转换为对象;
  • 不仅可以当做工具函数使用,还可以当做构造函数使用,即前面可以加new命令;
1
注意,通过var obj = new Object()的写法生成新对象,与字面量的写法var obj = {}是等价的。或者说,后者只是前者的一种简便写法。
  • Object的静态方法,例如Object.print,指部署在Object对象自身的方法;

  • Object的实例方法,即定义在Object.prototype对象上的方法。

    • Object.prototype.valueof()
    • Object.prototype.toString()
    • Object.prototype.toLocaleString()
    • Object.prototype.hasOwnProperty()
    • Object.prototype.isPrototypeof()
    • Object.prototype.propertyIsEnumerable()

属性描述对象

js提供了一个内部数据结构,用来描述对象的属性,比如是否可写可读可遍历,这个内部数据结构就称之为“属性描述对象”。

  • tip:Object.getOwnPropertyNames方法返回一个数组,成员是参数对象自身的全部属性的属性名,不管该属性是否可遍历。而这跟Object.keys的行为不同,Object.keys只返回对象自身的可遍历属性的全部属性名。
1
2
3
4
5
6
7
8
9
10
11
12
Object.keys([]) // []
Object.getOwnPropertyNames([]) // [ 'length' ]

Object.keys(Object.prototype) // []
Object.getOwnPropertyNames(Object.prototype)
// ['hasOwnProperty',
// 'valueOf',
// 'constructor',
// 'toLocaleString',
// 'isPrototypeOf',
// 'propertyIsEnumerable',
// 'toString']

上面代码中,数组自身的length属性是不可遍历的,Object.keys不会返回该属性。第二个例子的Object.prototype也是一个对象,所有实例对象都会继承它,它自身的属性都是不可遍历的。

  • Object.defineProperty(),Object.defineProperties()
    Object.defineProperty()方法接收三个参数,依次如下:
    • Object:属性所在的对象
    • propertyName:字符串,表示属性名
    • attributesObject:属性描述对象
1
2
3
4
5
6
7
8
9
10
11
var obj = Object.defineProperty({}, 'p', {
value: 123,
writable: false,
enumerable: true,
configurable: false
});

obj.p // 123

obj.p = 246;
obj.p // 123

Object.defineProperties()接收两个参数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
var obj = Object.defineProperties({}, {
p1: { value: 123, enumerable: true },
p2: { value: 'abc', enumerable: true },
p3: { get: function () { return this.p1 + this.p2 },
enumerable:true,
configurable:true
}
});

obj.p1 // 123
obj.p2 // "abc"
obj.p3 // "123abc"
  • Object.prototype.propertyIsEnumerable()
    返回一个布尔值,用来判断自身属性是否可以遍历。Object.keys是返回所有可以遍历的属性,不包括继承的属性,而Object.getOwnPropertyNames则是返回包括继承的属性在内的所有可以遍历的属性。

es6入门

len和const命令

len

len没有变量提升

必须先声明后才能使用,注意len变量只在块区域内有用,是块级作用域,而var不同,var具有全局作用域、函数作用域;

暂时性死区

1
2
3
4
5
6
var tmp = 123;

if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}

在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
非常隐蔽的“死区”:

1
2
3
4
5
function bar(x = y, y = 2) {
return [x, y];
}

bar(); // 报错

顶层对象的属性

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。但在ES6中,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

1
2
3
4
5
6
7
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

node js入门

Thank you for your accept. mua!
-------------本文结束感谢您的阅读-------------