JavaScript 学习笔记

Posted by API Caller on March 26, 2019

听说这个 JavaScript 的中文教程 很不错, 刚好也需要掌握 JS, 每天抽空学一点 (而且它的移动端体验也很不错, 可惜只能借助 Chrome 来同步进度), 本文记录一些我读后觉得自己容易忽视或者遗忘的知识点.

基础知识

运算符

运算符优先级列表

1
2
alert(2 + 2 + '1' ); // "41" 而不是 "221"
alert( '1' + 2 + 2); // "122" 而不是 "14"
1
alert( 5 || 1 && 0 ); // 5
1
alert( 1 && null && 2 ); // null

值的比较

1
2
3
4
5
6
7
let a = 0;
alert( Boolean(a) ); // false

let b = "0";
alert( Boolean(b) ); // true

alert(a == b); // true!
1
2
3
alert( null > 0 );  // (1) false
alert( null == 0 ); // (2) false
alert( null >= 0 ); // (3) true

这是因为相等性检测 == 和普通比较符 > < >= <= 的代码逻辑是相互独立的. 进行值的比较会把 null 转为数字, 因此它被转为了 0. 这就是为什么 (3) 中 null >= 0 返回 true, (1) 中 null > 0 返回 false.

1
2
3
alert( undefined > 0 ); // false (1)
alert( undefined < 0 ); // false (2)
alert( undefined == 0 ); // false (3)

原因如下:

  • (1) 和 (2) 中返回 false 是因为 undefined 在比较中被转换为了 NaN, 而 NaN 是一个特殊的数值型取值, 它与任何值进行比较都会返回 false.
  • (3) 中返回 false 是因为这是一个相等性检测, 而 undefined 只与 null 相等.

函数

1
2
3
4
alert(function(){
return
	1 | 2 & 0;
}()); // undefined

因为 JavaScript 默认会在 return 之后加分号.

Polyfills

对象

对象

常量对象的属性可以被修改, 因为 const 修饰的只是对象本身.

1
2
3
4
5
6
7
const user = {
  name: "John"
};

user.age = 25; // (*)

alert(user.age); // 25

计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let fruit = "apple";

let bag = {
  [fruit]: 5, // 属性名从 fruit 变量中计算
  // 我们在方括号中可以用更复杂的表达式: 
  [fruit + 'Computers']: 6, // bag.appleComputers = 5
  // 保留字段可以用作属性名, 除了 __proto__ (历史原因)
  for: 1,
  let: 2,
  return: 3,
  // __proto__ 这个赋值会被忽略
  __proto__: 4,
};

alert( bag.apple ); // 5 如果 fruit="apple"

相当于

1
2
3
4
5
let fruit = "apple";
let bag = {};

// 从 fruit 变量中获取值
bag[fruit] = 5;

test in objobj[test] === undefined 等效吗?

不等效.

1
2
3
4
5
6
7
let obj = {
  test: undefined
};

alert( obj.test ); //  它是 undefined, 所以难道它不存在吗?

alert( "test" in obj ); // true, 属性不存在!

对象引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let a = {};
let b = {};
let c = a;

alert(a == b); // false
alert(a === b); // false

alert(a == c); // true
alert(a === c); // true

alert(a == {}); // false
alert(a === {}); // false

alert({} == {}); // false
alert({} === {}); // false

因为只有引用同一个对象才会相等, 其余都是不同的对象.

由于引用的存在, 可以引出 深拷贝 的概念.

Symbol

  • 描述相同的 Symbol 也并不会相等, 需要相等可以用全局注册表: Symbol.for(key).
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    let id1 = Symbol("id");
    let id2 = Symbol("id");
    
    let id3 = Symbol.for("id");
    let id4 = Symbol.for("id");
    
    
    alert(id1 == id2); // false
    alert(id1 == id3); // false
    alert(id2 == id3); // false
    
    alert(id3 == id4); // true
    
  • Symbol 不支持 string 的隐式转换, 要主动调用 .toString().

  • Symbol 作为对象的属性唯一且隐藏, 并且不能被 for..in 遍历, 但是可以被 Object.assign 复制. 字面量的话需要方括号:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    let id = Symbol("id");
    
    let user = {
      name: "John",
      [id]: 123 
    };
    
    alert( user[id] ); // John
    alert( user[Symbol("id")] ); // undefined
    
  • 遍历方法也是有的, 例如 Object.getOwnPropertySymbols(obj)Reflect.ownKeys(obj)

this

  • 严格模式下的 this 值为 undefined, 在非严格模式 (没有使用 use strict) 的情况下, this 将会是全局对象 (浏览器中的 window, 我们稍后会进行讨论) . "use strict" 可以修复这个历史行为.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
      let user = {
        name: "John",
        hi() { alert(this.name); },
        bye() { alert("Bye"); }
      };
    	
      user.hi(); // John (the simple call works)
    	
      // 现在我们要判断 name 属性, 来决定调用 user.hi 或是 user.bye. 
      (user.name == "John" ? user.hi : user.bye)(); // Error!
    

    预期是 undefined, 但直接运行往往发现不符合预期, 这是因为没有 "use strict", 所以 this 实际上并不是预期的 undefined 而是全局对象 window.

  • 箭头函数没有自己this.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      let user = {
        firstName: "Ilya",
        sayHi() {
          let arrow = () => alert(this.firstName);
          arrow();
        }
      };
    	
      user.sayHi(); // Ilya
    

toPrimitive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let user = {
  name: "John",
  money: 1000,

  [Symbol.toPrimitive](hint) {
    alert(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  }
};

// 转换演示: 
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500

hint = “string”, “number” 和 “default” 中的一个

也可以重写 toStringvalueOf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let user = {
  name: "John",
  money: 1000,

  // 对于 hint="string"
  toString() {
    return `{name: "${this.name}"}`;
  },

  // 对于 hint="number" 或 "default"
  valueOf() {
    return this.money;
  }

};

alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500

new

  • new.target 可以用来判断函数是不是通过 new 调用的
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    function User() {
      alert(new.target);
    }
    
    // 不带 new: 
    User(); // undefined
    
    // 带 new: 
    new User(); // function User { ... }
    
  • new 参数为空的话, 括号可以省略

数据类型

基本类型的方法

构造函数 String/Number/Boolean 不推荐使用

1
2
let zero = new Number(0);
alert(Boolean(zero)) // true 意不意外

zero 在这里是一个对象.

数字类型

1
alert( 123456..toString(36) ); // 2n9c

第一个 . 是会识别成小数点

1
2
3
4
5
alert( NaN == NaN ); // false
alert( NaN === NaN ); // false

alert( Infinity == Infinity ); // true
alert( Infinity === Infinity ); // true

字符串

  • \u{NNNNNNNN} 一些罕见字符使用两个 unicode 符号进行编码, 最多占用 4 个字节. 这个长的 unicode 需要它周围的括号.

    1
    
      alert( "\u{1F60D}" ); // 😍, a smiling face symbol (another long unicode)
    
  • for..of 遍历字符

    1
    2
    3
    
      for (let char of "Hello") {
        alert(char); // H,e,l,l,o (char becomes "H", then "e", then "l" etc)
      }
    
  • 方括号 []charAt 的唯一区别是, 如果没有找到字符, [] 返回 undefined, 而 charAt 返回一个空字符串:

    1
    2
    3
    4
    
      let str = `Hello`;
    	
      alert( str[1000] ); // undefined
      alert( str.charAt(1000) ); // '' (an empty string)
    
  • Diacritical marks and normalization

    我对字符编码这么熟悉居然没看明白这段, 感觉是文章写得不清晰的锅, 一定是这样, 后面再补.

数组

  • 按照对象而不是按照数组的方式来用数组会导致性能下降:

    • 添加一个非数字的属性比如 arr.test = 5
    • 制造空洞, 比如: 添加 arr[0] 后添加 arr[1000] 而它们中间什么都没有
    • 以倒序填充数组, 比如 arr[1000], arr[999] 等等
  • push/pop 方法运行的比较快, 而 shift/unshift 比较慢.

  • 高效又简单的遍历语法 for..of:

    1
    2
    3
    4
    5
    6
    
      let fruits = ["Apple", "Orange", "Plum"];
    	
      // 迭代数组元素
      for (let fruit of fruits) {
        alert( fruit );
      }
    
  • length 属性要注意:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      let fruits = [];
      fruits[123] = "Apple";
    	
      alert( fruits.length ); // 124
    	
      let games = [];
      games.user = "Bob";
    	
      alert( games.length ); // 0
    
  • 并且 length 可写, 用 arr.length = 0; 清空数组甚至是推荐做法:

    1
    2
    3
    4
    5
    6
    7
    
      let arr = [1, 2, 3, 4, 5];
    	
      arr.length = 2; // 只剩 2 个元素
      alert( arr ); // [1, 2]
    	
      arr.length = 5; // 又把 length 加回来
      alert( arr[3] ); // undefined: 被截断的那些数值并没有回来
    
  • 数组没有 Symbol.toPrimitivevalueOf, 只能 toString:

    1
    2
    3
    
      alert( [] + 1 ); // "1"
      alert( [1] + 1 ); // "11"
      alert( [1,2] + 1 ); // "1,21"
    
  • 删除数组元素

    1
    2
    3
    4
    5
    6
    7
    8
    
      let arr = ["I", "go", "home"];
    	
      delete arr[1]; // remove "go"
    	
      alert( arr[1] ); // undefined
    	
      // now arr = ["I",  , "home"];
      alert( arr.length ); // 3
    
  • concat, 注意 Symbol.isConcatSpreadable:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
      let arr = [1, 2];
    	
      let arrayLike = {
        0: "something",
        length: 1
      };
    	
      alert( arr.concat(arrayLike) ); // 1,2,[object Object]
    	
      arrayLike[Symbol.isConcatSpreadable]= true;
    	
      alert( arr.concat(arrayLike) ); // 1,2,something
    	
      arrayLike[Symbol.isConcatSpreadable]= false;
    	
      alert( arr.concat(arrayLike) ); // 1,2,[object Object]
    
  • findfilter 语法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
      let users = [
        {id: 1, name: "John"},
        {id: 2, name: "Pete"},
        {id: 3, name: "Mary"}
      ];
    	
    // let user = users.find(function(item, index, array) {
    //  // 如果查询到返回 true
    //  return item.id == 1;
    // });
      let user = users.find(item => item.id == 1);
    	
      alert(user.name); // John
    
    // 返回前两个用户的数组
    let someUsers = users.filter(item => item.id < 3);
    
    alert(someUsers.length); // 2
    
  • map 语法:

    1
    2
    3
    4
    
      // 将每个元素转换为它的字符串长度
      let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length)
    
      alert(lengths); // 5,7,6
    
  • sort:

    1
    2
    3
    4
    5
    
      let arr = [ 2, 1, 15 ];
    	
      arr.sort( (a, b) => a - b )// arr.sort(function(a, b) { return a - b; });
    	
      alert(arr);  // 1, 2, 15
    

迭代

Iterable (可迭代对象) 是数组的泛化, 这个概念是说任何对象都可在 for..of 循环中使用.

  • 实际上是调用 Symbol.iterator
1
2
3
4
let str = '𝒳😂';
for (let char of str) {
    alert( char ); // 𝒳, 然后 😂
}
  • 也可以显示调用
1
2
3
4
5
6
7
8
9
10
11
12
let str = "Hello";

// 和下面代码完成的功能一致
// for (let char of str) alert(char);

let iterator = str[Symbol.iterator]();

while (true) {
  let result = iterator.next();
  if (result.done) break;
  alert(result.value); // 一个一个输出字符
}
  • 当然可以重写 Symbol.iterator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let str = {x: "Hello"};

str[Symbol.iterator] = function() {

  return {
    current: 0,
	
    // next() 将在 for..of 的每一轮循环迭代中被调用
    next() {
      // 它将会返回 {done:.., value :...} 格式的对象
      if (this.current++ == 0) {
        return { done: false, value: "Hello World!" };
      } else {
        return { done: true };
      }
    }
  };
};

let iterator = str[Symbol.iterator]();

while (true) {
  let result = iterator.next();
  if (result.done) break;
  alert(result.value); // 一个一个输出字符
}
  • 字符串迭代按照 UTF-16, 所以 Array.from 相当于按找字符分割.