1. 作用域
-
1.1 概念
作用域是一套规则,用于确定如何在当前作用域以及嵌套的子作用域中,根据标识符名称,进行变量查找。
-
1.2 LHS和RHS(查找类型)
LHS:左查找,对变量进行赋值 RHS:右查找,对变量进行引用
function fn(a) { // a = 2(隐式变量分配)是LHS var b = 3; // LHS return a + b; // RHS } fn(2); // fn() 是LHS
2. 词法作用域
-
2.1 概念
词法作用域是定义在词法阶段的作用域。
-
2.2 EVAL和WITH(修改作用域,尽量不要使用,消耗性能)
eval: 执行传入的参数,严格模式下无效
function fn(str) { eval(str); console.log(a); // 严格模式下报错 ReferenceError: a is not defined } fn('var a = 2'); // 2
with: 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域, 但在这个块内正常的var声明,并不会被限制在这个块的作用域中,而是会被添加到with所处的函数作用域中。
function fn(obj) { with(obj) { obj.a = 22 } } var obj1 = { a: 1 }, obj2 = { b: 3 }; fn(obj1); // obj1.a = 22 console.log(a); // a is not defined fn(obj2); // obj.a = undefined console.log(a); // a = 22 (非严格模式下,作用域对a进行了LHS查找,但在obj2和全局作用域中都未找到,便自动创建了一个全局变量a)
3. 函数作用域和块作用域
-
3.1 隐藏内部实现(最小暴露原则)
好处:私有化内部变量和函数;规避命名冲突。 坏处:污染该函数或对象所在的作用域;必须显示调用。 解决:匿名自执行函数。
-
3.2 立即执行函数表达式(IIFE)
使用:(function() {...})() / (function() {}()) 传参:(function() {})(window,...)
-
3.3 块级作用域
let:声明一个作用域被限制在块级中的变量、语句或者表达式。(没有变量声明提升) const: 声明创建一个常量的只读引用(变量标识符不能重新分配),作用域可以是全局或本地声明的块。
4. 提升
-
4.1 概念
包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。 只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。
-
4.2 变量声明提升
a = 2; var a; var a; => a = 2; console.log(a); // 2 // ------------------------------------------------------ console.log(b); // undefined var b; var b = 2; => console.log(b); b = 2;
-
4.3 函数声明提升(函数表达式不会提升)
fn(); // 1 function fn() { console.log(1) } // --------------------------------------------------------------- fn2(); // TypeError: fn2 is not a function (此时fn2 = undefined) var fn2 = function() { console.log(2) } // --------------------------------------------------------------- fn3(); // 3.1 functoin fn3() { // 函数声明提升 优先于 变量声明提升 var fn3; console.log(3.1) function fn3() { } console.log(3.1) => fn3() // 3.1 } fn3 = function() { fn3 = function() { console.log(3.2) console.log(3.2) }; } fn3(); // 3.2 fn3(); // 3.2( 函数表达式无提升,会覆盖前面声明的函数(注意函数调用位置!)) // --------------------------------------------------------------- fn4(); // 4.2 // 普通块内部的函数声明,通常会被提升到所在作用域顶部,不受条件判断控制(应尽量避免这种声明方法) if (true) { function fn4() { console.log(4.1) } } else { function fn4() { console.log(4.2) } }
5. 作用域闭包
-
5.1 概念
当函数可以记住并访问所在的词法作用域时,就产生了闭包。 MDN: 闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量。
function outer() { var a = 0; function inner() { a++; console.log(a); } return inner; } var add = outer(); add(); // 1 add(); // 2 add(); // 3
-
5.2 循环与闭包
例:分别每隔一秒每次一个打印1-5。
for (var i = 1; i <= 5; i ++) { setTimeout(function() { console.log(i) }, 1000 * i) } // 结果是每秒一次的频率输出五次6。(循环事件结束后,setTimeout才会执行;每次循环都引用了i,但i在所处作用域中只有一个,而此时i = 6。) // 使用闭包-------------------------------------------------------------------- for (var i = 1; i <= 5; i++) { (function(j) { setTimeout(function() { console.log(j) }, j * 1000) })(i) } // 新建一个变量------------------------------------------------------------------ for (var i = 1; i <= 5; i ++) { var a = 1; setTimeout(function() { console.log(a++) }, 1000 * i) } // 使用let(每轮循环都会根据上轮循环的值,重新声明i,)----------------------------- for (let i = 1; i <= 5; i++) { setTimeout(function() { console.log(i) }, 1000 * i) }
-
5.3 模块
-
5.3.1 概念
必须有外部的封闭函数,该函数必须被至少调用一次(每次调用都会创建一个新的模块实例); 封闭函数必须返回至少一个内部函数,这样函数才能在私有作用域中形成闭包,并且可以访问或修改私有的状态。
function common() { var str = 'string in common'; function doSomething() { console.log(str) } return { // 也可直接返回一个内部函数 doSomething: doSomething }; } var fn = common(); fn.doSomething(); // string in common
-
5.3.2 将模块转换成单例模式
var common = (function() { var str = 'string in common'; function doSomething() { console.log(str) } return { // 也可直接返回一个内部函数 doSomething: doSomething }; })(); common.doSomething(); // string in common
-
5.3.3 ES6的模块化
a.js function hello(name) { return 'hello' + name + '!' } export hello; --------------------------------- b.js function () {}
-
6. this
-
6.1 概念
this是在函数被调用时发生的绑定,指向什么完全取决于函数在哪里被调用。
-
6.2 四种绑定规则
-
6.2.1 默认绑定
被直接使用,不带任何修饰符的函数引用(独立调用)。只能使用默认绑定,此时 this 默认指向全局对象。 注意(严格模式下时,全局对象无法使用默认绑定)
function foo() { function foo() { console.log(this.a); => "use strict" } console.log(this.a) var a = 1; } foo(); // 1 var a = 2; foo(); // TypeError: Cannot read property 'a' of undefined //----------------------------------------------------------------- 疑问:这里的 foo()是运行在严格模式下,怎么还能打印出来this.a? 书中原话:虽然 this 的绑定规则完全取决于调用位置, 但是只有 foo() 运行在非 strict mode 下时,默认绑定才能绑定到全局对象; 严格模式下与 foo()的调用位置无关。 (PS:函数体整体是否处于严格模式!) function foo() { console.log( this.a ); } var a = 2; (function(){ "use strict"; foo(); // 2 })();
-
6.2.2 隐式绑定
函数的调用位置是否有上下文对象。
function foo() { console.log( this.a ); } var obj2 = { a: 42, foo: foo }; var obj1 = { a: 2, obj2: obj2 }; obj1.obj2.foo(); // 42 (PS:对象属性链中只有最后一层会影响函数的调用位置)
隐式丢失:被隐式绑定的函数会丢失绑定对象,从而应用到默认绑定规则。
var obj = { a: "obj inner", foo: function() { console.log(this.a); } }; var bar = obj.foo; // (PS:这里实际上引用的foo函数本身,所以调用bar()时,实际是独立调用) var a = "global"; bar(); // "global" setTimeout(obj.fn, 100); // "global" (原因同上) // -----------------------------------------------------------------------------
-
6.2.3 显式绑定
直接指定this的绑定对象。(apply/call/bind)
function foo(a,b,c) { console.log(this.a, a, b, c); } var obj = { a: 123 } foo.call(obj, 1,2,3) // 123 1 1 2 3 foo.call(obj, [1,2,3]) // 123 [1,2,3] undefined undefined foo.apply(obj, [1,2,3]); // 123 1 2 3 foo.bind(obj, 1, 2, 3)(); // 123 1 2 3 (PS:apply和call的唯一区别是传参,apply需要传一个数组,call需要枚举所有参数; bind会返回一个硬绑定的新函数)
API调用的上下文(函数提供了一个可选参数,作为执行的上下文,等同于bind(...))。 例:array.forEach(callback(currentValue[, index[, array]]), [, thisArg])
function foo(el) { console.log( el, this.id ); } var obj = { id: "awesome" }; var arr = [1, 2, 3]; arr.forEach(foo, obj); // 1 "awesome" 2 "awesome" 3 "awesome" arr.forEach(foo.bind(obj)); // 1 "awesome" 2 "awesome" 3 "awesome"
-
6.2.4 new绑定
使用new来调用函数,或者说是发生构造函数调用时,会构造一个新的对象,并将它绑定到构造函数调用中的this上。
function Person(name, age) { this.name = name; this.age = age } var p1 = new Person('张三', 21); // Person {name: "张三", age: 21}
-
6.2.5 优先级(从高到低)
new绑定:绑定到新创建的对象 显示调用:apply/call/bind的第一个参数 隐式调用:上下文对象 默认绑定:非严格模式下绑定到全局对象
-
6.2.6 箭头函数
箭头函数不适用上述的this四种标准规则,而是根据外层(函数或全局)作用域来决定this。
function foo() { function foo() { setTimeout(function() { setTimeout(() => { console.log(this.a); console.log(this.a); }, 1); => }, 1); } } var obj1 = { a: 1 }, var obj1 = { a: 1 }, a = 'global'; a = 'global'; foo.call(obj1) // global foo.call(obj1) // 1
-
7. 对象
-
7.1 内容
存储:在对象容器内部存储的是属性的名称(引用),就像指针,指向这些值真正的存储位置;属性名永远都是字符串,当使用String以外的其他值作为属性名时,首先会被转换成一个字符串。 访问:.a(属性访问);["a"](键访问);区别在于属性访问中的.操作符要求属性名满足标识符的命名规范; 计算属性:使用键访问的方式,通过表达式来计算属性名(Obj[prefix + 'foo']); 属性和方法:对象属性如果是一个函数,存储的同样是对该函数的引用,和其他函数唯一的区别是函数中的this可能会被隐式绑定到一个对象上去。
var obj = { foo: function() { console.log("This is foo"); } } obj[1] = "name is 1"; obj[obj] = ''name is obj"; obj[true] = "name is true" console.log(obj["1"]); // "name is 1" console.log(obj["Object Object"]); // "name is obj" console.log(obj["true"]); // "name is true" var bar = obj.foo; console.log(obj.foo); // ƒ () { console.log("This is foo"); } console.log(bar); // ƒ () { console.log("This is foo"); }
-
7.2 属性描述符
Object.defineProperty(obj, prop, descriptor):在对象上定义或修改一个属性,并返回这个对象 descriptor:{ writable: 属性值可修改, configurable: 属性可(通过defineProperty)配置(修改属性描述符)(值为false时不能删除), enumerable: 属性是否出现在对象的属性枚举中 (遍历) }
-
7.3 不变性(不可修改、重定义或删除;属性非引用类型)
对象常量不可变:结合 writable: false 和 configurable: false
var obj = { a: 1, b: "B" } Object.defineProperty(obj, 'a', { writable: false, configurable: false }) obj.a = 2; delete obj.a; console.log(obj.a) // 1
禁止扩展:Object.preventExtension() 禁止一个对象添加新属性并且保留已有属性,当前属性可删除,且只要可写就可改变。
var obj = { a: 1, b: "B" } Object.preventExtensions(obj); obj.c = 'c'; obj.a = 2; console.log(obj.c, obj.a) // undefined 2 delete obj.a console.log(obj.a) // undefined
密封: Object.seal() 封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置,当前属性不可删除,但只要可写就可改变。
var obj = { a: 1, b: "B" } Object.seal(obj); obj.c = 'c' obj.a = 2; console.log(obj.c, obj.a); // undefined 2 delete obj.a console.log(obj.a); // 2
冻结:Object.freeze() 不能向这个对象添加新的属性、不能修改已有属性值,不能删除已有属性,不能修改该对象已有属性的可枚举、可配置、可写性; 返回被冻结的对象。
-
7.4 Getter和Setter
分别在属性获取和设置时会调用; 当这两者都存在时,会被定义为“访问描述符”,同时会忽略该属性值的value和writable特性
var obj = { get a() { return this._a; }, set a(val) { this._a = val }, } console.log(obj.a); // undefined obj.a = 2; console.log(obj.a); // 2 ```
-
7.5 属性存在性
判断对象中是否存在某一属性(属性返回值同样可能是undefined)。
var obj = { get a() { return this._a; }, set a(val) { this._a = val }, } console.log(('a' in obj)); // true console.log(obj.hasOwnProperty('a')); // true console.log(obj.hasOwnProperty('_a')); // false; Object.defineProperty(obj, 'a', { enumerable: false }); console.log(obj.hasOwnProperty('a')); // true
8. 混合对象“类”
-
8.1 类理论相关
概念:类是一种设计模式,描述了一种代码的组织结构方式,即在软件中对真实世界中问题领域的建模的方式。 构造函数:类实例是由一个特殊的类方法构造的,这个方法通常和类名相同,被称为构造函数。 继承:子类包含父类行为的原始副本,但是也可以重写所有继承的行为甚至定义新行为。 多态:在继承链的不同层次,名称相同但是功能不同的函数,本质上引用的其实是复制的结果。