你不知道的JavaScript(上)笔记

六月 21, 202617JavaScript

之前读《你不知道的JavaScript上》做的笔记,迁移一下文章,方便后续复习使用,随便记录,自己看得懂就行

作用域

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询
通常将 JavaScript 归类为“动态”或“解释执行”语言,但事实上它是一门编译语言
传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”

1. 分词/词法分析(Tokenizing/Lexing)

这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)

2. 解析/语法分析(Parsing)

这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree, AST)

3. 代码生成

将AST转换为可执行代码的过程被称为代码生成。这个过程与语言、目标平台等息息相关

词法作用域

词法作用域就是定义在词法阶段的作用域
词法作用域意味着作用域是由书写代码时函数声明的位置来决定的
function foo (a) {
  var b = a * 2;
  
  function bar () {
    console.log(a, b, c);    
  }
    
  bar(b * 3);
}

foo(2) // 2, 4, 12
  1. 包含着整个全局作用域,其中只有一个标识符:foo
  2. 包含着foo所创建的作用域,其中有三个标识符:abarb
  3. 包含着bar所创建的作用域,其中只有一个标识符:c
JavaScript 中有 两个机制可以改变词法作用域:
  • eval(...): 对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时)
  • with: 通过将一个对象的引用当作作用域来处理,将对象的属性当作作用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)

动态作用域

动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套
function foo() {
  console.log(a); // 词法作用域会输出全局作用域中的 2
}


function bar() {
  var a = 3;
  foo();
}


var a = 2;

bar(); // 动态作用域会输出 3
JavaScript 只有词法作用域,他们的主要区别:
  • 词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的
  • 词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用

闭包

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这时就产生了闭包。
模块的两个特征:
  • 为创建内部作用域而调用了一个包装函数
  • 包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包

关于 this

this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到

绑定规则

1. 默认绑定

独立函数调用
function foo() {
  console.log(this.a);
}
var a = 2;
foo(); // 2
  • 非严格模式: this 指向全局对象
  • 严格模式: this 绑定到 undefined

2. 隐式绑定

调用位置有上下文对象
function foo() {
  console.log(this.a);
}
var obj = {
  a: 2,
  foo: foo,
};
obj.foo(); // 2

3. 显示绑定

使用函数的call(..)、apply(..)和bind(..)方法
function foo() {
  console.log(this.a);
}
var obj = {
  a: 2,
};
foo.call(obj); // 2

4. new 绑定

使用 new 来调用函数,会自动执行下面的操作:
  1. 创建一个全新的对象
  2. 这个新对象会执行 [[Proptotype]] 链接
  3. 这个新对象会绑定到函数调用的 this
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
function foo(a) {
  this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2

优先级

new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定
  1. 函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象
  2. 函数是否通过call、apply(显式绑定)或者硬绑定调用?如果是的话,this绑定的是指定的对象
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象

绑定例外

被忽略的 this
把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则
function foo() {
  console.log(this.a);
}
var obj = {
  a: 2,
};
foo.call(null); // 2
注意:使用null来忽略this绑定可能产生一些副作用。如果某个函数确实使用了this(比如第三方库中的一个函数),那默认绑定规则会把this绑定到全局对象(在浏览器中这个对象是window),这将导致不可预计的后果
间接引用
你有可能(有意或者无意地)创建一个函数的“间接引用”,在这种情况下,调用这个函数会应用默认绑定规则
function foo() {
  console.log(this.a);
}
var a = 2;
var o = { a: 3, foo: foo };
var p = { a: 4 };
o.foo(); // 3
// 间接引用最容易在赋值时发生
(p.foo = o.foo)(); // 2 

箭头函数

  • ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this
  • 箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。这其实和ES6之前代码中的self = this机制一样

对象

对象就是键/值对的集合。
JavaScript 中有七种主要类型:
  • string
  • number
  • boolean
  • null
  • undefined
  • symbol
  • object
“JavaScript中万物皆是对象”,这显然是错误的

内置对象

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error
nullundefined没有对应的构造形式,它们只有文字形式。相反,Date只有构造,没有文字形式

访问属性

  • .a 属性访问
  • ['a'] 键访问
  • obj[prefix + 'foo']可计算属性名(es6)
属性名永远都是字符串

复制对象

  • 浅复制: Object.assign(..)...运算符
  • 深复制: JSON.parse(JSON.stringify(obj)) (json 安全)

属性描述符

var myObject = { a: 1 };
Object.getOwnPropertyDescriptor(myObject, "a");
// {
//   value: 1,
//   writeable: true,
//   enumerable: true,
//   configurable: true,
// }
// 可以是用 Object.defineProperty 定义属性
Object.defineProperty(myObject, 'b', {
  value: 2,
  writable: true,
  configurable: true,
  enumerable: true,
});

myObject.b; // 2
  • writeable 决定是否可以修改属性的值
  • enumerable 属性是否会出现在对象的属性枚举中,比如说for..in循环
  • configurable 是否可以使用defineProperty(..)方法来修改属性描述符
configurable:false还会禁止删除这个属性

不变性

有时候你会希望属性或者对象是不可改变(无论有意还是无意)
所有的方法创建的都是浅不变性,也就是说,它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数,等),其他对象的内容不受影响,仍然是可变的
  • 对象常量
结合writable:falseconfigurable:false就可以创建一个真正的常量属性(不可修改、重定义或者删除)
var myObject = {};
Object.defineProperty(myObject, 'favorite number', {
  value: 2,
  writable: true,
  configurable: true,
  enumerable: true,
});
  • 静止扩展
禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions(..)
var myObject = {
  a: 2,
};
Object.preventExtensions(myObject);

myObject.b = 3;
  • 密封
Object.seal(..)会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..)并把所有现有属性标记为configurable:false
封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)
  • 冻结
Object.freeze(..)会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..)并把所有“数据访问”属性标记为writable:false,这样就无法修改它们的值

[[Get]] & [[Put]]

1. [[Get]]

对象属性访问,实际是进行[[Get]]操作:
  1. 对象默认的内置[[Get]]操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值;
  2. 如果没有找到名称相同的属性,就是遍历可能存在的[[Prototype]]链,也就是原型链;
  3. 如果无论如何都没有找到名称相同的属性,那[[Get]]操作会返回值undefined

2. [[Put]]

[[Put]]被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性:
**存在: **
  1. 属性是否是访问描述符 ?如果是并且存在setter就调用setter
  2. 属性的数据描述符中writable是否是false?如果是,在非严格模式下静默失败,在严格模式下抛出TypeError异常
  3. 如果都不是,将该值设置为属性的值
不存在:
给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值
myObject.foo = 'bar'; // 以此为例
  1. 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性并且没有被标记为只读(writable:false),那就会直接在myObject中添加一个名为foo的新属性,它是屏蔽属性
  2. 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable:false),那么无法修改已有属性或者在myObject上创建屏蔽属性
  3. 如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一定会调用这个setterfoo不会被添加到(或者说屏蔽于)myObject,也不会重新定义foo这个setter

3. geter & setter

getter是一个隐藏函数,会在获取属性值时调用
setter也是一个隐藏函数,会在设置属性值时调用
在ES5中可以使用gettersetter部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上
当你给一个属性定义gettersetter或者两者都有时,这个属性会被定义为“访问描述符”。对于访问描述符来说,JavaScript会忽略它们的valuewritable特性,取而代之的是关心setget(还有configurableenumerable)特性
var obj = {
  // 给 a 定义一个 getter
  get a() {
    return this.a;
  },
  // 给 a 定义一个 setter
  set a(val) {
    this.a = val * 2;
  }
};

obj.a = 2;
obj.a; // 4

4. 存在性

属性访问返回值为undefined,两种情况:
  1. 值有可能是属性中存储的undefined
  2. 属性不存在所以返回undefined
var obj = { a: 2 };


// in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中
('a' in obj) // true
('b' in obj) // false
// hasOwnProperty(..) 只会检查属性是否在 obj 对象中,不会检查 [[Prototype]] 链
obj.hasOwnProperty('a'); // true
obj.hasOwnProperty('b'); // false

混合对象“类”

面向类的设计模式:实例化、继承和多态
JavaScript 中只有对象,并不存在可以被实例化的“类”。一个对象并不会被复制到其他对象,它们会被关联起来
模拟类的复制行为,显式混入和隐式混入

显示混入

function mixin(sourceObj, targetObj) {
  for (var key in sourceObj) {
     // 不存在的情况下复制
     if (!(key in targetObj)) {
       targetObj[key] = sourceObj[key];
     }
  }
  return targetObj;
}
显式混入是 JavaScript 中一个很棒的机制,不过它的功能也没有看起来那么强大。虽然它可以把一个对象的属性复制到另一个对象中,但是这其实并不能带来太多的好处,无非就是少几条定义语句,而且还会带来我们刚才提到的函数对象引用问题

隐式混入

var Somthing = {
  cool: function() {
    tyhis.greeting = 'hello world';
  }
}
Somthing.cool();
Somthing.greeting; // hello world

var Another = {
  cool: function() {
    // 隐式把 Something 混入 Another
    Something.cool.call(this);
  }
};
Another.cool();
Another.greeting; // hello world
隐式混入利用了 this 的重新绑定功能,但是 Something.cool.call(this) 仍然无法变成相对(而且更灵活的)引用,所以使用时千万要小心。通常来说,尽量避免使用这样的结构,以保证代码的整洁和可维护性

原型

[[Prototype]]

JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用
  • 要访问对象中并不存在的一个属性,[[Get]]操作就会查找对象内部[[Prototype]]关联的对象。这个关联关系实际上定义了一条“原型链”(有点像嵌套的作用域链),在查找属性时会对它进行遍历
  • 所有普通的[[Prototype]]链最终都会指向内置的Object.prototype

行为委托

假设我们需要在软件中建模一些类似的任务(“XYZ”、“ABC”等)

类理论

定义一个通用父(基)类,可以将其命名为Task,在Task类中定义所有任务都有的行为。接着定义子类XYZ和ABC,它们都继承自Task并且会添加一些特殊的行为来处理对应的任务
 // 伪代码
class Task {
  id;
  Task(ID) { id = ID; }
  outputTask() { output(id); }
}

class XYZ inherits Task {
 label;
 XYZ(ID, Label) { super(ID); label = Label; }
 outputTask() { super(); output(label); }
}
class ABC inherits Task {
  // ...
}

委托理论

定义一个名为Task的对象,它会包含所有任务都可以使用(写作使用,读作委托)的具体行为。接着,对于每个任务(“XYZ”、“ABC”)你都会定义一个对象来存储对应的数据和行为。把特定的任务对象都关联到Task功能对象上,让它们在需要的时候可以进行委托
Task = {
  setId: function(ID) { this.id = ID; },
  outputID: function() { console.log(this.id); } 
};

// 让 XYZ 委托 Task
XYZ = Object.create(Task);
XYZ.prepareTask = function(ID, Label) {
  this.setID(ID);
  this.label = Label;
};
XYZ.outputTaskDetails = function() {
  this.outputID();
  console.log(this.label);
};
对象关联风格:
  1. [[Prototype]]委托中最好把状态保存在委托者(XYZ、ABC)而不是委托目标(Task)
  2. 尽量避免在[[Prototype]]链的不同级别中使用相同的命名,否则就需要使用笨拙并且脆弱的语法来消除引用歧义
  3. 委托行为意味着某些对象(XYZ)在找不到属性或者方法引用时会把这个请求委托给另一个对象(Task

比较思维模型

面向对象风格

function Foo(who) {
  this.me = who;
}

Foo.prototype.indentify = function() {
  return 'I am ' + this.me;
};

function Bar(who) {
  Foo.call(this, who);
}

Bar.prototype = Object.create(Foo.prototype);

Bar.prototype.speak = function() {
  alert('hello, ' + this.indentify() + '.');
}

var b1 = new Bar('b1');
var b2 = new Bar('b12');

b1.speak();
b2.speak();

对象关联风格

Foo = {
  init: function(who) {
    this.me = who;
  },
  indentify: function() {
    return 'I am ' + this.me;
  }
}

Bar = Object.create(Foo);
Bar.speak = function() {
  alert('hello, ' + this.indentify() + '.');
};

var b1 = Object.create(Bar);
b1.init('b1');
var b2 = Object.create(Bar);
b2.init('b2');

b1.speak();
b2.speak();

更好的语法

在ES6中我们可以在任意对象的字面形式中使用简洁方法声明
var LoginController = {
  errors: [],
  getUser() {
    // ...
  },
  getPassword() {
    // ...
  }
  // ...
};
反词法
简洁方法有一个非常小但是非常重要的缺点
var Foo = {
  bar() {},
  baz: function baz() {},
};
// 去掉语法糖之后
var Foo = {
  bar: function() {},
  baz: function baz {},
}
// 由于函数对象本身没有名称标识符,所以bar()的缩写形式(function()..)
// 实际上会变成一个匿名函数表达式并赋值给bar属性。
// 相比之下,具名函数表达式(function baz()..)会额外给.baz属性附加一个词法名称标识符baz
匿名函数没有name 标识符:
  1. 调试栈更难追踪
  2. 自我引用(递归、事件(解除)绑定,等等)更难
  3. 代码(稍微)更难理解
简洁方法没有第1和第3个缺点,简洁方法无法避免第2个缺点,它们不具备可以自我引用的词法标识符
END

评论

欢迎分享你的看法

发表评论
欢迎留言,请友好互动
点击添加表情:

全部评论

0

还没有评论

来说两句吧!