前言:
毕业到入职腾讯已经差不多一年的时光了,接触了很多项目,也积累了很多实践经验,在处理问题的方式方法上有很大的提升。随着时间的增加,愈加发现基础知识的重要性,很多开发过程中遇到的问题都是由最基础的知识点遗忘造成,基础不牢,地动山摇。所以,就再次回归基础知识,重新学习JavaScript相关内容,加深对JavaScript语言本质的理解。日知其所亡,身为有追求的程序员,理应不断学习,不断拓展自己的知识边界。本系列文章是在此阶段产生的积累,以记录下以往没有关注的核心知识点,供后续查阅之用。
2017
02/26
apply(参数为数组) 、 call (参数需一一列举)、bind 三者都是用来改变函数的this对象的指向的;apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;apply 、 call 、bind 三者都可以利用后续参数传参;bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。
02/27
匿名函数:在栈追踪中不显示有意义的函数名,难以调试,没有函数名不好引用自身,缺少了可读性和可理解性。
块级作用域:with、try{} catch(){}、let、const。let:不会有变量提升。
02/28
JS代码执行分为两个阶段:编译阶段、执行阶段。
包含函数和变量的所有声明都会在任何代码被执行前首先被处理,声明本身会被提升,而赋值或其他运行逻辑会留在原地。函数声明和函数表达式不同,函数表达式不会整体提升,只会把表达式赋给的变量声明提升,函数表达式赋值还留在原来位置。
函数声明和变量声明都会被提升,但是一个值得注意的细节(这个细节可以出现在有多个 “重复”声明的代码中)是函数会首先被提升,然后才是变量。
03/01
闭包使得函数可以继续访问定义时的词法作用域。无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
for (var i = 1; i <= 5; i++) { (function(j) { setTimeout(function timer() { console.log(j); }, j * 1000); })(i); }
在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。
for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i); }, i * 1000); }
03/02
模块有两个主要特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回 值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭 包。
主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。(this也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。 动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。
03/03
ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定 this,具体来说,箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这 其实和 ES6 之前代码中的 self = this 机制一样。 简单来说,箭头函数在涉及 this 绑定时的行为和普通函数的行为完全不一致。它放弃了所 有普通 this 绑定的规则,取而代之的是用当前的词法作用域覆盖了 this 本来的值。
03/04
需要明确的是,this 在任何情况下都不指向函数的词法作用域。在 JavaScript 内部,作用 域确实和对象类似,可见的标识符都是它的属性。但是作用域“对象”无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部。 之前我们说过 this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调 用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。 this 既不指向函数自身也不指向函数的词法作用域,this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
03/06
Object.create(null) 和 {} 很 像, 但 并 不 会 创 建 Object. prototype 这个委托,所以它比 {}“更空”。
typeof null == “object” :原理是这样的,不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判 断为 object 类型,null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“object”。
03/07
如果你试图向数组添加一个属性,但是属性名“看起来”像一个数字,那它会变成一个数值下标(因此会修改数组的内容而不是添加一个属性)。
对于 JSON 安全(也就是说可以被序列化为一个 JSON 字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法: var newObj = JSON.parse( JSON.stringify( someObj ) );
要注意有一个小小的例外:即便属性是 configurable:false,我们还是可以把 writable 的状态由 true 改为 false,但是无法由 false 改为 true。
不变性:
- 1. 对象常量 : 结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、 重定义或者删除)。
- 2. 禁止扩展如果你想禁止一个对象添加新属性并且保留已有属性, 可 以使用 Object.prevent Extensions(..)。
- 3. 密封 Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用 Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false。
- 4. 冻结 Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们 的值。
在 ES5 中可以使用 getter 和 setter 部分改写默认操作,但是只能应用在单个属性上,无法应用在整个对象上。getter 是一个隐藏函数,会在获取属性值时调用。setter 也是一个隐藏函数,会在设置属性值时调用。
03/08
“可枚举”就相当于“可以出现在对象属性 的遍历中”。
更加强硬的方法来进行判 断:Object.prototype.hasOwnProperty. call(myObject,"a"),它借用基础的 hasOwnProperty(..) 方法并把它显式绑定到 myObject 上。
propertyIsEnumerable(..) 会检查给定的属性名是否直接存在于对象中(而不是在原型链 上)并且满足 enumerable:true。
Object.keys(..) 会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..) 会返回一个数组,包含所有属性,无论它们是否可枚举。
遍历数组下标时采用的是数字顺序(for 循环或者其他迭代器),但是遍历对象属性时的顺序是不确定的,在不同的 JavaScript 引擎中可能不一样。因此, 在不同的环境中需要保证一致性时,一定不要相信任何观察到的顺序,它们是不可靠的。
03/09
使用 for..in 遍历对象时原理和查找 [[Prototype]] 链类似,任何可以通过原型链访问到 (并且是 enumerable)的属性都会被枚举。使用 in 操作符来检查属性在对象 中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举)。 因此,当你通过各种语法进行属性查找时都会查找 [[Prototype]] 链,直到找到属性或者查找完整条原型链。
所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype。由于所有的“普通” (内置,不是特定主机的扩展)对象都“源于”(或者说把 [[Prototype]] 链的顶端设置为) 这个 Object.prototype 对象,所以它包含 JavaScript 中许多通用的功能。
如果 foo 不直接存在于 myObject 中而是存在于原型链上层时 myObject.foo = "bar" 会出现的三种情况:
- 1. 如果在 [[Prototype]] 链上层存在名为 foo 的普通数据访问属性并且没有被标记为只读(writable:false),那就会直接在 myObject 中添加一个名为 foo 的新 属性,它是屏蔽属性。
- 2. 如果在 [[Prototype]] 链上层存在 foo,但是它被标记为只读(writable:false),那么 无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会 抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
- 3. 如果在 [[Prototype]] 链上层存在 foo 并且它是一个 setter,那就一定会 调用这个 setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这 个 setter。 如果你希望在第二种和第三种情况下也屏蔽 foo,那就不能使用 = 操作符来赋值,而是使 用 Object.defineProperty(..)来向 myObject 添加 foo。
03/10
Foo.prototype 默认有一个公有并且不可枚举的属性 .constructor,这个属性引用的是对象关联的函数。 可以看到通过“构造函数”调用 new Foo() 创建的对象也有一个 .constructor 属性,指向 “创建这个对象的函数”。 在普通的函数调用前面加上 new 关键字之后,就会把这个函数调用变成一个“构造函数 调用”。实际上,new 会劫持所有普通函数并用构造对象的形式来调用它。 换句话说,在 JavaScript 中对于“构造函数”最准确的解释是,所有带 new 的函数调用。 这是一个很不幸的误解。实际上,.constructor 引用同样被委托给了 Foo.prototype,而 Foo.prototype.constructor 默认指向 Foo。 Foo.prototype 的 .constructor 属性只是 Foo 函数在声明时的默认属性。如果 你创建了一个新对象并替换了函数默认的 .prototype 对象引用,那么新对象并不会自动获 得 .constructor 属性。
function Foo() { /* .. */ } Foo.prototype = { /* .. */ }; // 创建一个新原型对象 var a1 = new Foo(); a1.constructor === Foo; // false! a1.constructor === Object; // true!
委托给委托链顶端的 Object.prototype。这个对象 有 .constructor 属性,指向内置的 Object(..) 函数。
Bar.prototype = Object.create( Foo.prototype )
如果使用内置的 .bind(..) 函数来生成一个硬绑定函数的话, 该函数是没有 .prototype 属性的。在这样的函数上使用 instanceof 的话, 目标函数的 .prototype 会代替硬绑定函数的 .prototype。
Foo.prototype.isPrototypeOf(a);
b.isPrototypeOf(c);
.__proto__ 实际上并不存在于你正在使用的对象中 ,存在于内置的 Object.prototype 中(它是不可枚举的)。
03/11
Object.create(null) 会创建一个拥有空( 或者说 null)[[Prototype]] 链接的对象,这个对象无法进行委托。这些特殊的空 [[Prototype]] 对象通常被称作“字典”,它们完全不会受到原型链的干扰,因此非常适合用来存储数据。
对象的链接被称为“原型链”, JavaScript 中这个机制的本质就是对象之间的关联关系。
委托行为意味着某些对象(XYZ)在找不到属性或者方法引用时会把这个请求委托给另一 个对象(Task)。
对象关联可以更好地支持关注分离(separation of concerns)原则,创建和初始化并不需要 合并为一个步骤。
// 让 Foo 和 Bar 互相关联 Foo.isPrototypeOf(Bar); // true Object.getPrototypeOf(Bar) === Foo; // true
行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制。也就是说,我们可以选择在 JavaScript 中努 力实现类机制,也可以拥抱更自然的 [[Prototype]] 委托机制。
对象关联(对象之前互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把它们抽象成类。对象关联可以用基于 [[Prototype]] 的行为委托非常自然地实现。
首先,你可能会认为 ES6 的 class 语法是向 JavaScript 中引入了一种新的“类”机制,其 实不是这样。class 基本上只是现有 [[Prototype]](委托!)机制的一种语法糖。 也就是说,class 并不会像传统面向类的语言一样在声明时静态复制所有行为。
class 语法无法定义类成员属性(只能定义方法)。class 语法仍然面临意外屏蔽的问题。class 很好地伪装成 JavaScript 中类和继承设计模式的解决方案,但是它实际上起到了反作 用:它隐藏了许多问题并且带来了更多更细小但是危险的问题。