目录
第一部分 作用域和闭包
第1章 作用域是什么
第2章 词法作用域
第3章 函数作用域和块作用域
第4章 提升
第5章 作用域闭包
第二部分 this和对象模型
第1章 关于this
第2章 this全面解析
第3章 对象
第4章 混合对象 “类”
第5章 原型
第6章 行为委托
完结语:2021年7月2日,终于写完了。我原本是想用一周的时间看完这本上卷的,也是没想到前前后后竟然花了十多天的时间。还好,收获还是挺多的,书中的内容的确让我对JavaScript这门语言有了更深的认识。
这个知识梳理适合那些已经看完了《你不知道的JavaScript 上卷》,但仍觉得一头雾水的人,我也看了很多博主写了这本书的知识梳理,大都是对书中内容的简单堆砌,及简单的加以说明,这让我这个JavaScript小白在理解书中的概念时总是一头雾水,可能是初学的原因,我看到书中很多概念的时候,就像学了一个数学定理,也知道这个定理的意思,但是却不知道这个数学定理怎么使用,也无从知道书中表达的意思,故在数学学习的过程中我经常渴望知道别人的理解。因此,我决定自己写一篇《你不知道的JavaScript》知识梳理,认真揣摩作者的意思,以便更通俗的理解书中概念,尽量领会书中的每一章要表达什么。这本书看的较为仓促。若有错误,望评论区指正,我会仔细查阅资料并给予修改。
再过不久,我便会看剩下的两本书,我也会在博客里写出我的理解,希望看客们可以关注我,并可以给我一些指正。
我觉得本章的难点:
先来回答本章的标题:
作用域是什么?
作用域就是变量和函数的可访问范围,控制着变量和函数的可见性与生命周期。使用数学中的函数理解,说白了作用域就是取值范围。
function foo(a) { console.log(a); //2 }; foo(2);
上述代码是引擎和作用域的对话。这里注意, LHS和RHS是对于赋值操作( foo()函数的执行也可以理解为赋值操作,相当于是将一个函数赋值给foo,然后执行foo),即赋值操作左边是否有我们要存放的容器,赋值操作右边是否有我们操作的值
即LHS查询:
执行foo()时,对foo的查询(是一个函数)
执行foo(2)时,对foo内的a进行查询(a是参数)
RHS查询:
执行foo(2)时,对a的赋值操作
执行console.log()时,对log()方法的引用
执行console.log(a)时,对a赋值的操作
由上述我们可以理解:为什么RHS查询会出现两种错误ReferenceError和TypeError了
前者是因为找不到赋值操作,后者是因为找到了,但是进行了不合理的操作,例如这样
let foo = null; foo(2);
因此,我觉得TypeError错误可以理解为RHS查询成功了,但是进行了非法操作。当然这些书中提到了,我这里是梳理了一边逻辑。
这是小测验的代码,看完书里的解释,又理解了上面的内容,这个小测验就是小菜一碟
function foo (a){ var b = a; return a + b; }; var c = foo( 2 );
LHS查询:即赋值操作左边的容器。RHS查询:即赋值操作右边的值。
LHS:c=,a=2(foo(2)的隐式操作,就是找有没有a这个参数,因为即便函数有形参也可以不写实参),b=
其实我困惑在为什么a=2的赋值操作右边的=2不定义为RHS查询;我这里猜一下,是因为=2不用查,因为2本身就是值,查询的实质是找,变量是存储数据的容器,因此才需要找。
RHS:foo这个函数,=a,return a+b的a和b(return相当于是一个赋值操作a+b是赋值操作的值),
本章重要知识:
1.JavaScript是编译语言,其编译发生在代码执行前的几微妙。即编译和执行几乎同步。因此JavaScript会用尽各种办法进行性能性能优化。
2.引擎、编译器、作用域。简单了解作用即可。
3.LHS和RHS查询
4.作用域嵌套:即在嵌套的作用域中,引擎的LHS查询会实行冒泡规则查找
5.不成功的LHS查找会自动创建一个全局变量(非严格模式),严格模式下会抛出ReferenceError异常,不成功的RHS查询会抛出ReferenceError异常
我认为本章的难点:
先通俗回答本章的标题:
词法作用域:即是语言给代码的书写提供了一套书写规则,按照这个规则而形成的作用域就是词法作用域,说白了就是代码块的作用范围。例如书写函数时,函数内部的数据,外部一般无法访问,这个函数作用范围就是词法作用域的一部分。
在这张图中,这三个作用域气泡就是词法作用域,即是按照代码的书写规则来,形成的代码块的作用范围。
作用域气泡严格包含,即只会出现气泡包含,不会出现气泡交叉
解释一下书中附录A中提到的动态作用域
1.JavaScript只有词法作用域!(但this机制和动态作用域很像)
function foo() { console.log(this.a); //2 console.log(a); //2 } function bar() { var a = 3; foo(); } var a = 2; bar();
这里如果JavaScript是动态作用域,那么输出的将会是3,因为动态作用域会查找调用栈。
2.词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。
关于eval欺骗词法详解
//eval欺骗词法 function foo(str, a) { eval(str); //欺骗 console.log(a, b); } var b = 2; foo('var b = 3;', 1)
基本上没啥好说的,就是会直接把传入的字符串转换为js代码,即此时的eval(str)完全等价于var b = 3;
说一下这个函数的妙用,eval会执行字符串,当我们的到的数据是可以执行的字符串时,那么使用eval还是挺炫的。
关于with欺骗词法的详解
先说理解,with会关联一个对象,并且with内的变量均会映射到关联对象的同名属性。with内的变量若没有映射到同名属性,则会LHS查询向上冒泡
function foo(obj) { with(obj) { //会创建作用域气泡 var b = 3; //实际上会被定义在foo函数内 // let c = 6;//会被限制在with内 a = 2; //这个a会直接找obj的属性a console.log(a); //2 console.log(obj.a); //undefined console.log(b); //3 console.log(obj.b); //undefined }; // console.log(a) //ReferenceError console.log(b); //3 // console.log(c); }; var o1 = { a: 3 } var o2 = { b: 3 }; foo(o1); console.log(o1.a); //2 // // console.log(a); //RHS查询,ReferenceError // foo(o2); // console.log(o2.a); //undefined // console.log(o2.b); //undefined // console.log(a); //2,window.a
1.这句话的前半句就是说with会创建一个作用域气泡;后半句就是说传入对象的属性会关联到with作用域气泡内的同名标识符。就如,在第6行代码中a可以直接输出为2;而此时,在代码11行和代码23行,输出a的值就是ReferenceError。
就是说with内的赋值操作的规则是:赋值操作优先赋值到传入with内对象的对应属性,当没有对应的属性时,赋值操作会冒泡LHS查询对应的变量容器。
因此,当我们在with内书写c=2时,若传入的对象没有c属性,with会冒泡查询;若有c属性时,with会停止查找,赋值到此属性。
2.这段话:就是在代码3行,使用var声明变量b后,在with的作用域气泡外,即foo函数的气泡内可以访问变量b,即是此处var声明的变量属于with所在的作用域气泡。
当你理解到这里。with的欺骗词法在于,它会给传入对象创建一个作用域气泡,即词法作用域。
就是说,在这个气泡内a=2是不可以在外部访问的,而若此对象没有这个属性,a=2可以修改外部作用域气泡的变量。
说一下with的妙用,把eval嵌入到with内并给eval传入修改关联with对象属性的代码,就可以动态修改对象的数据了。但不要这样做,会导致性能极度下降。
希望读者自行将上述代码放行注释运行查看,有些细节,需要运行代码自我体会。
本章重要知识:
1.词法作用域就是定义在词法阶段的作用域,即作用域是由书写代码时函数声明的位置决定。
2.作用域会在找到第一个匹配的标识符时停止,因此会有“遮蔽效应。
3.欺骗词法:eval和with(会创建作用域气泡)
4.JavaScript引擎会在编译阶段进行数项的性能优化
5.欺骗词法会使得JavaScript引擎无法在编译时对作用域查找进行优化。
6.作用域查找始终从运行时所处的最内部作用域开始。
我认为本章的难点:
1.立执行函数的小例子
undefined = true; //企图覆盖undeined console.log(undefined); //undefined,可见修改不成功 (function IIFE(undefined) { console.log(undefined) //undefined, var a; if (a === undefined) { console.log('Undefined is safe here!') } })(); //没有传入参数
这个例子,即是想覆盖默认值,但是实际发现,undefined并不会被覆盖
2.IIFE函数可以倒置代码的执行顺序
var a = 2; (function IIFE(def) { def(window); })(function def(global) { var a = 3; //会有遮蔽效应 console.log(a); //3 console.log(global.a); //2 });
这段代码其实很好理解,即def被当作参数传递给了IIFE,并在IIFE内部执行,两次输出的不同实质是遮蔽效应导致
本章重要知识:
1.无论标识符声明出现在作用域的何处买这个标识符所代表的标量或函数都将依附于所处作用域的气泡。(这里说的原理,应该就是变量提升和函数优先)
2.每一个函数内部都是一个作用域气泡,基于此,我们可以将代码包裹在函数作用域气泡内,这种基于作用域的隐藏方法即为最小特权原则。
function foo() { function bar(a) { i = 3; //修改for循环中所属作用域中的i for (var i = 0; i < 10; i++) { bar(i * 2); //此时造成无限循环了 } } }
这里就是出现了变量冲突,我们应该让bar函数内部的i成为本地变量。
3.很多库为了避免变量冲突,大都是定义一个独特的对象,让每个变量、方法成为变量的属性。
4.具名函数和匿名函数表达式
//注意,因为是表达式,所以结尾需要使用分号 //匿名函数表达式 (function() { var a = 3; console.log(a); }); //具名函数表达式 (function foo() { var a = 3; console.log(a); });
5.立执行函数表达式
//形式一 (function foo(a) { console.log(a); }(3)); //形式二 (function foo(a) { console.log(a); })(3);
6.JavaScript语言表面上没有块级作用域的相关功能,实际上with(仅关联对象的属性同名标识符具有块级,语言表述能力有限,请自行到上述with解释查看。书中把这种表现形式称为块级,我觉得不够严谨,因为在with内声明的非同名属性标识符实际上是定义在with所在作用域气泡内的)、try/catch的catch均会创建一个块级作用域。try/catch的性能不佳
7.let关键字可以将变量绑定到所在的任意作用域中,即let可以为其声明的变量隐式地劫持了所在的块作用域。
我认为本章的难点:
1.块内作用域的函数提升机制。JavaScript未来可能会改变这种机制,因此尽量不要在块内声明函数。
// foo(); //TypeError: foo is not a function var a = true; console.log(foo); //undefined if (a) { //实际上被提升到if块内了 function foo() { console.log('a'); } } else { function foo() { console.log('b'); } } console.log(foo);
本章重要知识点:
1.JavaScript编译器编译前会优先找到所有声明,并用合适的作用域将其关联起来。因此,包括变量和函数在内的所有声明都会在任何代码被执行前优先被处理。
//写的是这 foo(); function foo() { console.log(a); //undefined var a = 2; } //实际运行的是这 function foo() { var a; console.log(a); a = 2; } foo();
2.函数声明和变量声明均会被提升,即函数名和变量名同名时,函数提升会导致忽略同名的变量。就是函数提升的优先级较高。
此时我真正理解了什么是闭包,才体会到了 启示 —— JavaScript中的闭包无处不在!!! 这句话的含义。
我认为本章的难点:
function foo() { var a = 2; function bar() { console.log(a) } return bar; } foo()(); //2 var baz = foo(); //2 baz();
闭包就是JavaScript引擎阻止了正在被引用函数的垃圾回收,即代码中bar持续被引用,也即是foo的作用域气泡持续被引用。
就行例子中,foo函数已经被执行了,foo函数理应会被回收,但是JavaScript引擎阻止了回收。
我们通俗的理解闭包,即为函数被封闭在作用域气泡内,函数的调用却在作用域气泡之外。
上述,是我试图用书中解释理解闭包,但总觉得过于学术,因此,我的理解:
0.我把函数的执行,理解为函数名是函数执行的开关,函数名()就会打开这个开关,“触发”函数。
1.函数的执行、变量的取值均是一种“触发”;即函数名(),直接引用变量名,这些均是对函数及变量的“触发”。
2.但是函数和变量不同,函数的执行是函数名(),函数名即为变量(我称变量为容器),仅是存储函数的容器(实际上存放的是函数的指针,即函数的快捷方式),请看下述代码
(function() { function foo() { var a = 2; function bar() { console.log(a) } return bar; } console.log(foo); //函数foo console.log(window.foo); //undefined foo = 12; console.log(foo) //12 console.log(window.foo); //undefined })();
说明foo可以被赋值,且不是window对象的属性,故可以断定foo就是一个变量(容器),当然,其实学过C语言或者其他语言的都是知道这一点的,我仅是为了更加确定而已。
3.因此,我说函数的“触发“需要两个条件:函数名和()。()会自动找到变量内的函数并执行
//闭包的深度解析 function foo() { var a = 2; function bar() { console.log(a) } return bar; } foo()(); //2 var baz = foo(); //2 baz();
在这段代码中,bar仅仅是一个存放函数的变量,执行函数foo后会返回bar变量。
4.现在拉回来,由于JavaScript是词法作用域,函数的执行,其执行的作用域是在函数定义的作用域气泡内。
5.即函数foo是把bar函数的“触发”开关返回到了外面。外面使用()执行了foo内函数bar,接着就是正常词法作用域的操作了。
function setupBot(name, selector) { $(selector).click(function activator() { console.log('Activating' + name); }) } setupBot('Closure Bot 1', '#bot_1')
理解到这里,上面这段代码之所以是闭包,原因即是,内部回调函数的执行是由外部的setupBot控制的。
6.解释下循环和闭包
for (var i = 1; i <= 5; i++) { // var j = i; setTimeout(function timer() { console.log(j) }, i * 1000); }
这里仅有两级作用域,全局作用域(for循环内)和回调函数内的作用作用域,根据词法作用域规则,回调函数作用域会共享全局作用域,故最终都会输出6
for (var i = 1; i <= 5; i++) { (() => { var j = i; setTimeout(function timer() { console.log(j) }, i * 1000); })(); }
这里使用了立执行函数(ES6语法),因此,此时有三级作用域,即全局作用域(for内)、立执行函数的作用域、回调函数的作用域。详细解释下:每次迭代会有5个立执行函数作用域同级和5个回调函数作用域同级,且分别是包含关系
故每次迭代时定义的 j 变量分别属于5个立执行函数的作用域,就是说没有 j 的进行数据存储的话,回调函数会由词法作用域规则向上冒泡查找i,这样还是输出5个6
for (var i = 1; i <= 5; i++) { ((j) => { setTimeout(function timer() { console.log(j) }, j * 1000); })(i); }
这个改进之后的代码,事实上说明了,函数的每次执行,参数都会进行一次绑定。和使用let很像
for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i) }, i * 1000); }
7.模块和闭包连用。模块就是一个封闭函数返回了一个对象(函数本质也是对象),因此模块内部形成闭包,故可以通过返回的对象对模块内部持续引用,进行操作。
var foo = (function CoolModule(id) { function change() { //修改公共API publicAPI.identify = identify2; }; function identify1() { console.log(id); }; function identify2() { console.log(id.toUpperCase()); }; var publicAPI = { change: change, identify: identify1 }; return publicAPI; //返回模块 })('foo module'); //此时foo对其模块内部持续引用 //故下述方法执行了模块内部的函数 foo.identify(); foo.change(); foo.identify();
理解了闭包之后,理解模块变得轻而易举。上述代码没啥说的,就是模块产生了闭包,然后对模块内部的持续引用,进行的一些操作。
8.书中 现代模块机制 的一个代码案例
//现代的模块机制 var MyModules = (function Manager() { var modules = {}; function define(name, deps, impl) { for (var i = 0; i < deps.length; i++) { deps[i] = modules[deps[i]]; }; modules[name] = impl.apply(impl, deps); }; function get(name) { return modules[name]; }; return { define: define, get: get, } })(); MyModules.define('bar', [], function() { function heool(who) { return 'Let me introduce: ' + who; }; return { //返回的是对象 heool: heool }; }); MyModules.define('foo', ['bar'], function() { var hungry = 'hippo'; function awesome() { console.log(bar.hello(hungry).toUpperCase()); }; return { //返回的是对象 awesome: awesome }; }); var bar = MyModules.get('bar'); var foo = MyModules.get('foo'); console.log(bar.hello('hippo')); //Let me introduce:hippo foo.awesome();
其实挺好理解的,就是有点绕:
先说defined函数:一个参数name是会给modules添加一个属性,第二个参数deps就是对modules的属性遍历,并且将遍历的东西传入到第三个参数impl(回调函数),第三个参数也会变成modules的属性。
再说get函数:就是简单的获取moduls的属性
这里说一下代码中第一次执行MoModules.define()时传入了一个空数组,显然modules获取不到属性,则下面的impl.apply()传入的deps就是空了;也就是说apply的第二个参数是一个空,apply的第二个参数是传递给原函数的,即是第一次执行的时候,回调函数内没有传值。第二次同理。
当然,还是需要自己认真体会代码的。
9.关于附录C this词法的一些理解
在setTimeout中this指向了window,官网给出的答案是:由setTimeout()
调用的代码运行在与所在函数完全分离的执行环境上
在我看来,obj.cool是指向了cool函数,js引擎在执行obj.cool()时,是一种obj修饰的调用;由此foo也指向了cool函数,故此时foo是对cool函数的直接引用,foo()执行时是不加修饰的调用;
因此,我认为setTimeout的参数就是相当于foo一样,是对cool函数的不加修饰的调用。
//附录C this词法 var obj = { id: 'awesome', cool: function coolFn() { console.log(this.id); // console.log(this); } }; var id = 'not awesome'; obj.cool(); //awesome //this的绑定取决于调用位置,故这里是对coolFn函数的直接引用,即直接调用 var foo = obj.cool; foo(); //not awesome // 可见这里也是直接引用 setTimeout(obj.cool, 100); //not awesome // console.log(window.id)
本章知识点:
1.基本都是难点,要记得不多,但均需要理解。
2.模块模式的必要条件:
①必须由外部的粉笔函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
②封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
3.模块文件中的内容会被当做包含在作用域闭包中一样处理。类似于函数闭包模块.
本章就是讲了一些对this的误区
我认为的本章难点:
无
书中本章知识点:
1.为什么要使用this?this提供了一种更简洁且易于复用的API设计,我们可以通过更改this指向,隐式的“传递”一个对象的引用。
2.this不指向自身。
// 关于this function foo(num) { console.log('foo: ' + num); //记录foo被调用的次数 this.count++; }; foo.count = 0; var i = null; for (i = 0; i < 10; i++) { if (i > 5) { foo(i); } } console.log(foo.count); //0
3.this在任何情况下都不指向函数的词法作用域,作用域存在于JavaScript引擎内部。
4.经典的this错误,它试图跨越边界
var obj = {} function foo() { var a = 2; this.bar(); } function bar() { // LHS查询对象的时候貌似会自动创建属性,应该时新改的。 console.log(this.a); //undefined console.log(obj.a); //undefined } // this.foo(); //undefined //我觉得定义在全局作用域的函数也会成为全局对象的属性 foo();
5.this的绑定和函数的声明位置没有任何关系,只取决于函数的调用方式 .即this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
本章就是说了this的指向的规则,及怎么修改this指向,
我认为的本章难点:
1.书中的MDN的bind实现,最后的部分没看懂
if (!Function.prototype.bind) { //检擦Function的原型是否有bind方法 Function.prototype.bind = function(OThis) { //定义bind方法 if (typeof this !== 'function') { //检查调用者是否是函数 //与 ECMAScript 5最接近的 //内部 IsCallacle 函数 throw new TypeError( //如果不是函数,则抛出一个错误,这个函数会停止后续代码的执行 'Function.prototype.bind - what is trying' + 'to be bound is not callable' ); }; //分割bind函数的参数,slice可以分割arguments这个伪数组,1代表从第2个索引开始分割arguments var aArgs = Array.prototype.slice.call(arguments, 1), fToBind = this, //bind调用者 fNOP = function() {}, // fBound = function() { return fTobind.apply( ( this instanceof fNOP //fNOP的原型链是否在this原型链上 && OThis ? this : OThis //oThis是否存在 ), aArgs.concat( Array.prototype.slice.call(arguments) ) ); }; fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; } }
2.书中软绑定源码,我现在还没能完全理解
//软绑定 if (!Function.prototype.softBind) { Function.prototype.softBind = function(obj) { var fn = this; //捕获所欲curried参数 var curried = [].slice.call(arguments, 1); var bound = function() { return fn.apply( (!this || this === (window || global)) ? obj : this, curried.concat.apply(curried, arguments) ); }; bound.prototype = Object.create(fn.prototype); return bound; } } function foo() { console.log('name: ' + this.name); } var obj = { name: 'obj' }, obj2 = { name: 'obj2' }, obj3 = { name: 'obj3' }; var fooOBJ = foo.softBind(obj); //绑定到了obj上 fooOBJ(); //name:obj obj2.foo = foo.softBind(obj);//绑定到obj上 obj2.foo(); //name:obj2,貌似和隐式绑定差不多 fooOBJ.call(obj3); //name:obj3 setTimeout(obj2.foo, 100); //name:obj
3.箭头函数会捕获调用上级函数的this,且无法修改。箭头函数可以像bind一样确保函数的this被绑定到指定对象,实质是使用词法作用域取代了this机制,内部有类似于var self = this;这句代码
//箭头函数 function foo() { //返回一个箭头函数 return (a) => { //this继承自foo() console.log(this.a) } } var obj1 = { a: 2 }; var obj2 = { a: 3 }; var bar = foo.call(obj1); bar.call(obj2); //2
本章重要知识点:
1.调用位置就是函数在代码中被调用的位置(而不是声明位置)。需要分析调用栈。
2.函数运行在“非严格模式”下,函数会执行默认绑定;但在严格模式下调用函数,则不影响函数的默认绑定 。区分一下运行和调用。默认绑定就是全局函数默认绑定到全局对象上。
3.当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。对象的属性引用链中只有上一层或者说最后一层在调用位置中起作用
function foo() { console.log(this.a) } var obj2 = { a: 42, foo: foo } var obj1 = { a: 2, obj2: obj2 } obj1.obj2.foo(); //42
3. 隐式绑定造成this丢失的实质是对一个函数的直接引用(不带任何修饰的函数调用)
4.回调函数的函数可能会修改this,流行的JavaScript库中事件处理器常会把回调函数的this强制绑定到触发事件的DOM元素上。我是用jQuery发现并没有像后半句所说的那样
window.addEventListener('load', function() { console.log(this); //window document.querySelector('p').addEventListener('click', function(e) { function foo() { console.log(this); //window } foo(); //这里就是绑定到触发的DOM元素上 console.log(this); //p console.log(e.target); //p }) }) $(function() { console.log(this); //#document,貌似是jQuery的文档对象 $('p').on('click', function(e) { function foo() { console.log(this); //window } foo(); console.log(this); //p console.log(e.target); //p }) })
5.硬绑定call和apply2。硬绑定就是使用新函数对call和apply语句进行了隔离。
//硬绑定, function foo() { console.log(this.a) } var obj = { a: 2, foo: foo }; var a = 3; var bar = function() { // foo(); return foo.call(obj); //2 }; bar(); setTimeout(bar, 100); //2 //硬绑定的bar不可能再修改它的this。 //上面这句和废话一样,bar现在是一个新函数,这些操作均是对新函数的操作,怎么会修改foo的this bar.call(window); //2
6.new操作符调用的是普通函数,即所有函数均可使用new调用。不存在所谓的”构造函数“,只有对于函数的”构造调用“,new的操作:
1.创建(或者说构造)一个全新的对象
2.这个新对象会被执行[[Prototype]]连接
3.这个新对象会被绑定到函数调用的this
4.如果函数没有返回其他对象,那么new表达式的函数调用会自动返回这个新对象
function foo(a) { this.a = a; } var bar = new foo(2); //返回了一个新对象,因此可以bar.a访问新对象的属性 console.log(bar.a); //2
7.判断this:
①函数是否在new中调用(new 绑定)?如果是的话this绑定的是新创建的对象。
var bar = new foo();
②函数是否通过call、apply(显示绑定)或着硬绑定调用?如果是的话,this绑定的是指定的对象。
var bar = foo.call(obj2)
③函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
var bar =obj1.foo();
④如果均不是的话,使用默认绑定。如果严格模式下,就绑定到undefined,否则绑定到全局对象。
var bar = foo();
8.柯里化:把除了第一个参数(第一个参数用于绑定this)之外的其他参数都传给下层的函数(这种技术称为”部分应用“,是“柯里化”的一种)
9.JavaScript中创建一个空对象最简单的方法都是object.create(null)。这么做不会创建object.prototype这个委托,比{}“更空”。
10.对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。
11.硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显示绑定来修改this。
有一种常见的说法“JavaScript中万物皆是对象”,这显然是错误的。
本章就是讲了对象的一些操作:创建、配置、封印
本章难点:
1.Symbol.iterator:为每一个对象定义了默认的迭代器。该迭代器可以被 for-of 循环使用。for-of会找内置的@@iterator对象并调用next()方法来遍历数据
//自定义对象迭代器 var myObject = { a: 2, b: 3 }; Object.defineProperty(myObject, Symbol.iterator, { enumerable: false, writable: false, configurable: true, value: function() { var o = this; var idx = 0; var ks = Object.keys(o); return { next: function() { return { value: o[ks[idx++]], done: (idx > ks.length) }; } }; } }) //手动遍历myObject var it = myObject[Symbol.iterator](); console.log(it.next()); //{value:2,done:false} console.log(it.next()); //{value:3,done:false} console.log(it.next()); //{value:undefined,done:true} //for-of遍历 for (var v of myObject) { console.log(v) }; //2,3
代码的意思就是定义了一个Symbol.iterator属性,这个属性的value是个函数,函数内部有各种操作。文字表达起来有点费劲,可以尝试运行代码自行体会
本章知识点:
1.JavaScript的六种主要类型:string,number,boolean,null,undefined,object。
typeof null;之所以返回Object,是因为JavaScript中二进制前三位均为0,就会被判定为object类型,而null的二进制表示全为0,自然被判定为object了
2.内置对象:String,Number,Boolean,Object,Function,Array,Date,RegExp,Error。
3.JavaScript会自动将字符串,数字等使用方法时,会转换为内置对象,如‘I am string’,实际上会进行new String(‘I am string’)
4.在对象中,属性名永远是字符串,如果你使用string(字面量,字符串的存储是固定的)以外的其他值作为属性名,那它首先会被转换为一个字符串;
5.数组也是对象,因此可以给数组添加属性,但是如果试图给数组添加一个类数字属性,进行键访问的时候,此属性会变成一个数值下标
var myArray = ['foo', 'turnip', 43]; myArray['3'] = 'kkk'; myArray.length; //4 myArray[3]; //43
6.Object.defineProperty官方描述:Object.defineProperty。该方法可以配置对象
'use strict' //修改只读属性,会报错 var myObject = {}; Object.defineProperty(myObject, 'a', { value: 2, writeable: false, //不可写,只读 configurable: true, //可配置的 enumerable: true //可枚举 }); myObject.a = 3; console.log(myObject.a); //2
7.结合 writeable:false和configurable:false 就可以创建一个真正的常量属性(不可修改、重定义或删除)
var myObject = {}; Object.defineProperty(myObject, 'FAVORITE_NUMBER', { value: 42, writable: false, configurable: false })
8.Object.preventExtensions官方解释:Object.preventExtensions。该方法可以让一个对象变的不可扩展,也就是永远不能再添加新的属性。
var myObject = { a: 2 }; Object.preventExtensions(myObject); myObject.b = 3; console.log(myObject.b); //undefined
9.Object.seal()官方解释:Object.seal。该方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置、不可删除。当前属性的值只要原来是可写的就可以改变。
10.Object.freeze()官方解释:Object.freeze()。这个方法是可以应用在对象上的级别对高的不可变性。
11.getter和setter均是一个隐藏函数,前者获取属性时调用,后者设置属性时调用。
var myObject = { //给a定义一个getter get a() { return this._a_; }, set a(val) { this._a_ = val * 2; } } myObject.a = 2; console.log(myObject.a);//4 console.log(myObject._a_); //4,创建了一个属性
11可枚举 就相当于 可以出现在对象属性的遍历中。for-in会静默,但是for循环可以。
propertyIsEnumerable()会检查给定的属性名是否直接存在于对象中(而不是原型链上)并且满足enumerable:true
Object.keys()会返回一个数组,包含所有可枚举的属性,Object,getOwnpropertyNames()会返回一个数组,包含所有属性,无论他们是否可枚举;
Object.prototype.hasOwnProperty和in的区别在于是否查找[[Prototype]]链
object.keys()和Object.getOwnPropertyNames()都只会查找对象直接包含的属性
var myObject = {}; Object.defineProperty(myObject, 'a', //让a像普通属性一样可以枚举 { enumerable: true, value: 2 }); Object.defineProperty(myObject, 'b', //让a像普通属性一样可以枚举 { enumerable: false, value: 2 }); console.log(myObject.b); //3 console.log(('b' in myObject)); //true console.log(myObject.hasOwnProperty('b')); //true for (var key in myObject) { console.log(key, myObject[key]); }; //'a' 2 console.log(myObject.propertyIsEnumerable('a')); //true console.log(myObject.propertyIsEnumerable('b')); //false console.log(Object.keys(myObject)); //['a'] console.log(Object.getOwnPropertyNames(myObject)); //['a','b']
只要理解了this指向和对象,这一章的内容理解起来很容易,都是一些概念而已。
本章难点:
1.多态就是任何方法均可以引用继承层次中高层的方法,及继承使得方法可以在多类中映射。
2.显示多态就是把显示的将父类的方法引入子类,但此时方法显示绑定的对象是子类
本章知识点:
1.类/继承描述了一种代码的住址结构——一种在软件中对真实世界中问题领域的建模方法,面向对象编程强调的是数据和操作数据的行为本质上是互相关联的。
2.JavaScript中并没有类,JavaScript只是提供了一些近似类的语法,因此JavaScript的类是一种设计模式(类本身就是一种设计模式)。如ES6的class
3.类实例是由一个特殊的类方法构造的,这个方法名通常和类同名,被称为构造函数。
4.类的继承其实就是复制
原型的实质就是对象之间的关联关系。
本章难点:
1.原型的本质就是一种关联关系,原型链就是关联树,即JavaScript引擎会查找原型链
var anotherObject = { a: 2 }; //创建一个关联到anotherObject的对象 var myObject = Object.create(anotherObject); //注意此时myObject.a并不存在,但是JavaScript引擎会在原型链上查找该属性 console.log(myObject.a); //2
2.隐式屏蔽
var anotherObject = { a: 2 }; var myObject = Object.create(anotherObject); console.log(anotherObject.a); //2 console.log(myObject.a); //2 console.log(anotherObject.hasOwnProperty('a')); //true console.log(myObject.hasOwnProperty('a')); //false console.log(anotherObject); console.log(myObject); //属于anotherObject下一级 myObject.a++; //myObject.a= myObject.a + 1 console.log(anotherObject.a); //2 console.log(myObject.a); //3 console.log(myObject.hasOwnProperty('a')); //true
3.new的关联,new的操作,就是创建了一个关联到其他对象的新对象。实际上我们并没有复制“类“,只是让他们关联而已
function Foo() {} var a = new Foo(); //new操作后,都会被关联到Foo.prototype对象上 console.log(Object.getPrototypeOf(a) === Foo.prototype); //true
本章知识点:
1.使用for-in 遍历对象时原理和查找[[Prototype]]链类似,任何可以通过原型链访问到的属性都会被枚举。使用in操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链。
2.所有普通的[[Prototype]]链最终都会指向内置的Object.prototype。
3.函数不是构造函数,但当且仅当使用new时,函数调用会变成”构造函数调用“。
4.Object.create()会创建一个新对象,并把它关联到我们指定的对象上
//兼容的Object.create if (!Object.create) { Object.create = function(o) { function F() {}; //声明空函数 F.prototype = o; return new F(); } }
5.内部委托。避免明明当前对象没有这个属性,但是却神奇的可以调用。也使得API设计更加清晰
var anotherObject = { cool: function() { console.log('cool!'); } } var myObject = Object.create(anotherObject); myObject.doCool = function() { this.cool(); //内部委托 } myObject.doCool();
这一章没什么可说的,就是对委托这种设计模式的讲解,理解了this机制、对象、原型之后,书中的代码也并没有什么难点。
知识回顾:
1.如果在第一个对象上没有找到需要的属性或者方法引用,JavaScript引擎会继续在[[Prototype]]关联的对象上进行查找。
2.JavaScript的原型链机制的本质就是对象与对象之间的关联关系。
本章难点:
1.类理论和委托理论的对比:
//类理论(伪代码) class Task { id; //函数Task() Task(ID) { id = ID; } outputTask() { output(id); } }; class XYZ inherits Task { label; //构造函数XYZ XYZ(ID, Label) { super(ID); label = Label; } outputTask() { super(); output(label); } }; class ABC inherits Task { //... }; //委托理论 Task = { setID: function(ID) { this.id = ID; }, output: 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); }; //ABC = Object.create(Task); //ABC ... = ...
可以看出,类理论:当实例化子类时,子类的实例会复制父类和子类的行为 。 可参考java类原理
委托理论:”子类“(对象)通过[[Prototype]]委托给了”父类“,当调用子类中没有的方法时,会通过委托关系查找关联的对象,又由于调用位置触发this的隐式绑定规则,故this的绑定仍是”子类“,而这正是我们想要的结果。由此看来委托的设计模式更加简洁。
本章知识点:
无