Open zhangzheng-zz opened 3 years ago
词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段基本能够知道全部标识符(变量、函数)在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。
欺骗词法作用域不推荐使用:JavaScript
中有两个机制可以“欺骗”词法作用域:eval(..)
和with
。运行时修改了词法作用域。(例如属性遮蔽)
这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。不要使用它们。
函数作用域: 函数是JavaScript
中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。
e.g
function foo() {
var a = 2;
function bar() {}
}
window.a
// undefined
window.bar()
// Uncaught TypeError: window.bar is not a function
块级作用域: 函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指{ .. } 内部)。从ES3 开始,try/catch 结构在catch 分句中具有块作用域。 e.g
try {} catch (error) {
// 块作用域
function f() {
console.log("f")
}
var a = 1
}
window.a
// undefined
window.f()
// Uncaught TypeError: window.f is not a function
ES6中变量块级作用域的产生:
一般来说,在 {...}中通过 var
声明的变量属于全局属性,挂在全局作用域上:
if(true) {
// 不会有块级作用域
function f() {
console.log("f")
}
var a = 10
}
window.a
// 10
window.f()
// f
ES6中,可以通过let
和const
来产生变量的块级作用域:
if(true) {
// 有块级作用域
let k = 10
}
undefined
window.k
//undefined
通过立即自执行函数可以隐蔽变量以及函数:
(function() {
var a = 1
function bar() {
console.log("bar")
}
})()
第1章讲过,var a = 1
被JavaScript
引擎看作两个过程:
var a
a = 1
函数与变量提升: 所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升, 并且,函数提升优先于变量提升。 e.g
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
会输出1 而不是2 !这个代码片段会被引擎理解为如下形式:
function foo() {
console.log( 1 );
}
foo(); // 1
foo = function() {
console.log( 2 );
};
注意,var foo 尽管出现在function foo()... 的声明之前,但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。但出现在后面的函数声明还是可以覆盖前面的(函数声明)。
一个普通块内部的函数声明通常会被提升到所在作用域的顶部: e.g
foo(); // "b"
var a = true;
if (a) {
function foo() { console.log("a"); }
}
else {
function foo() { console.log("b"); }
}
注意:函数表达式不会提升 e.g
// 函数不提升
f()
var f = function() {
console.log("f")
}
// 函数提升
f()
function f(){
console.log("f")
}
//TypeError: f is not a function
闭包:当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。 循环与闭包: 下面来看一个经典的问题:
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
运行时会以每秒一次的频率输出五次6 使用 IIFE 进行改造,创造出作用域:
for (var i = 1; i <= 5; i++) {
(function () {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})();
}
运行结果还是一样,五次6
如果作用域是空的,那么仅仅将它们进行封闭是不够的。
每次迭代,使用使用变量j
来存储,形成独立的词法作用域
for (var i = 1; i <= 5; i++) {
(function () {
var j = i;
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})();
}
或者
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
运行结果为每秒增加1输出 使用变量的块作用域解决:
for (let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
模块模式:
封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并 且可以访问或者修改私有的状态。
function CoolModule() {
// 私有变量
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join(" ! "));
}
return {
doSomething: doSomething,
doAnother: doAnother,
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
var MyModules = (function Manager() {
var modules = {};
/**
* @param {String} name 函数名
* @param {Array} deps 引用的(函数作用域)的函数名
* @param {Function} impl 函数
*/
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 hello(who) { return "Let me introduce: " + who; } return { hello: hello, }; });
MyModules.define("foo", ["bar"], function (bar) { 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(); // LET ME INTRODUCE: HIPPO
this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
绑定规则: 找到调用位置,然后判断需要应用下面四条规则中的哪一条
一、默认绑定:无法应用其他规则时的默认规则
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 2
function foo() {
"use strict";
console.log( this.a );
}
var a = 2;
foo(); // TypeError: this is undefined
function foo() {
console.log( this.a );
}
var a = 2;
(function(){
"use strict";
foo(); // 2
})();
二、隐式绑定:调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含 foo() 作为 obj 的属性被调用
function foo() {
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2
对象属性引用链中只有最顶层或者说最后一层会影响调用位置:
function foo () {
console.log(this.a)
}
const obj2 = {
a: 42,
foo: foo
}
const obj1 = {
a: 2,
obj2: obj2
}
obj1.obj2.foo() // 42
隐式丢失的现象:
function foo () {
console.log(this.a)
}
const obj = {
a: 2,
foo: foo
}
const bar = obj.foo // 函数别名!
const a = 'oops, global' // a 是全局对象的属性
// bar 实际引用了 foo 相当于 foo()
bar() // "oops, global"
发生在回调函数中:
function foo () {
console.log(this.a)
}
function doFoo (fn) {
// fn 其实引用的是foo
fn() // <-- 调用位置!
}
const obj = {
a: 2,
foo: foo
}
const a = 'oops, global' // a 是全局对象的属性
doFoo(obj.foo) // "oops, global"
三、显式绑定:call、apply、bind
自定义 bind 函数:
function foo (something) {
console.log(this.a, something)
return this.a + something
}
var obj = {
a:2
}
// 自定义 bind 绑定函数
function bind (fn, obj) {
return function () {
return fn.apply(obj, arguments)
}
}
var bar = bind( foo, obj );
bar(3) // 5
ES5 中提供了内置的方法Function.prototype.bind,可以直接使用:foo.bind(obj,2)()
四、new绑定 new 执行了如下步骤:
obj.__proto__
)执行构造函数的原型(foo.prototype
)function foo (a) {
this.a = a
}
const bar = new foo(2)
console.log(bar.a) // 2
规则优先级: new 绑定 > 显式绑定 (call、apply、bind)> 隐式绑定(绑定上下文obj等调用函数)> 默认绑定 特殊情况: 在某些场景下this 的绑定行为会出乎意料,你认为应当应用其他绑定规则时,实际上应用的可能是默认绑定规则。更安全的编码是将 this 显式绑定到 null、undefined(等同于默认绑定 window 等)或者一个特殊对象: call 默认绑定:
function foo () {
console.log(this.a)
}
const a = 2
foo.call(null) // 2
apply 和 bind 柯里化:默认绑定
function foo (a, b) {
return a + b
}
var fn = foo.bind(null, 3)
foo.apply(null, [4,5])
fn(4)
有些调用可能在无意中使用默认绑定规则。如果想“更安全”地忽略this 绑定,你可以使用一个DMZ 对象,比如:
o = Object.create(null)
以保护全局对象
ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this 绑定(无论this 绑定到什么)。这其实和ES6 之前代码中的self = this 机制一样。
function foo () {
setTimeout(() => {
// 这里的this 在此法上继承自foo()
console.log(this.a)
}, 100)
}
const obj = {
a: 2
}
foo.call(obj) // 2
等同于:
function foo () {
const self = this // lexical capture of this
setTimeout(function () {
console.log(self.a)
}, 100)
}
const obj = {
a: 2
}
foo.call(obj) // 2
在对象中,属性名永远都是字符串。如果你使用 string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。即使是数字也不例外
var myObject = {};
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
ES6 增加了可计算属性名,可以在文字形式中使用[] 包裹一个表达式来当作属性名:
var prefix = "foo";
var myObject = {
[prefix + "bar"]: "hello",
};
myObject["foobar"]; // hello
经典问题:深拷贝 & 浅拷贝
深拷贝: var newObj = JSON.parse( JSON.stringify( someObj ) )
浅拷贝:Object.assign(..) 方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。它会遍历一个或多个源对象的所有可枚举的自有键并把它们复制(使用= 操作符赋值)到目标对象,最后返回目标对象。
JSON.stringify 深拷贝的缺点:
1、如果obj
里面有时间对象,则拷贝后时间将只是字符串的形式,而不是对象的形式;
2、如果obj
里有RegExp
、Error
对象,则序列化的结果将只得到空对象;
3、如果obj
里有函数,undefined
,则序列化的结果会把函数或 undefined
丢失;
4、如果obj
里有NaN
、Infinity
和-Infinity
,则序列化的结果会变成null
5、JSON.stringify()
只能序列化对象的可枚举的自有属性,例如 如果obj
中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))
深拷贝后,会丢弃对象的constructor
;
6、如果对象中存在循环引用的情况也无法正确实现深拷贝。
ES5 开始,所有的属性都具备了属性描述符
var myObject = {
a: 2,
};
Object.getOwnPropertyDescriptor(myObject, "a");
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
使用Object.defineProperty(..)
来添加一个新属性或者修改一个已有属性(如果它是configurable
)并对特性进行设置。
var myObject = {};
Object.defineProperty(myObject, "a", {
value: 2,
writable: true,
configurable: true,
enumerable: true,
});
myObject.a; // 2
configurable:false
还会禁止删除这个属性for..in
循环。如果把 enumerable
设置成false
,这个属性就不会出现在枚举中,虽然仍然可以正常访问它。writable:false
和configurable:false
就可以创建一个真正的常量属性(不可修改、重定义或者删除):var myObject = {};
Object.defineProperty(myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false,
});
Object.preventExtensions(..)
:var myObject = {
a: 2,
};
Object.preventExtensions(myObject);
myObject.b = 3;
myObject.b; // undefined
密封:
Object.seal(..)
会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..)
并把所有现有属性标记为configurable:false
。所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。
冻结
Object.freeze(..)
会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..)
并把所有“数据访问”属性标记为writable:false
,这样就无法修改它们的值。
可以在不访问属性值的情况下判断对象中是否存在这个属性:
var myObject = {
a: 2,
};
"a" in myObject; // true
"b" in myObject; // false
myObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("b"); // false
in
操作符会检查属性是否在对象及其[[Prototype]]
原型链中。相比之下,hasOwnProperty(..)
只会检查属性是否在myObject
对象中,不会检查[[Prototype]]
链。
枚举:
在数组上应用for..in
循环有时会产生出人意料的结果,因为这种枚举不仅会包含所有数值索引,还会包含所有可枚举属性。最好只在对象上应用for..in
循环,如果要遍历数组就使用传统的for
循环来遍历数值索引。
var myObject = {};
Object.defineProperty(
myObject,
"a",
// 让a 像普通属性一样可以枚举
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 让b 不可枚举
{ enumerable: false, value: 3 }
);
myObject.propertyIsEnumerable("a"); // true
myObject.propertyIsEnumerable("b"); // false
Object.keys(myObject); // ["a"]
Object.getOwnPropertyNames(myObject); // ["a", "b"]
propertyIsEnumerable(..)
会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:true
。
Object.keys(..)
会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..)
会返回一个数组,包含所有属性,无论它们是否可枚举。
in
和hasOwnProperty(..)
的区别在于是否查找 [[Prototype]] 链,然而,Object.keys(..)
和Object.getOwnPropertyNames(..)
都只会查找对象直接包含的属性。
小结:
hasOwnProperty(..)
Object.keys(..)
(返回属性名组成的数组) Object.getOwnPropertyNames(..)
(返回属性名组成的数组) 都只会查找对象直接包含的属性in
会查找 [[Prototype]] 链propertyIsEnumerable
检查给定的属性名是否直接存在于对象中并且满足enumerable:true
ES5 中增加了一些数组的辅助迭代器,包括forEach(..)
、every(..)
和some(..)
。每种辅助迭代器都可以接受一个回调函数并把它应用到数组的每个元素上,唯一的区别就是它们对于回调函数返回值的处理方式不同。
forEach(..)
会遍历数组中的所有值并忽略回调函数的返回值every(..)
会一直运行直到回调函数返回false
(或者“假”值)some(..)
会一直运行直到回调函数返回 true
(或者“真”值)
every(..)
和some(..)
中特殊的返回值和普通for
循环中的break
语句类似,它们会提前终止遍历。forEach(..)
中 break
无效!!!ES6 增加了一种用来遍历数组的for..of
循环语法(如果对象本身定义了迭代器的话也可以遍历对象):
var myArray = [1, 2, 3];
for (var v of myArray) {
console.log(v);
}
// 1
// 2
// 3
mixin | extend
// 非常简单的mixin(..) 例子:
function mixin(sourceObj, targetObj) {
for (var key in sourceObj) {
// 只会在不存在的情况下复制
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
var Vehicle = {
engines: 1,
ignition: function () {
console.log("Turning on my engine.");
},
drive: function () {
// this 指向 Vehicle
this.ignition();
console.log("Steering and moving forward!");
},
};
var Car = mixin(Vehicle, {
wheels: 4,
drive: function () {
// this 绑定到 Car 上
Vehicle.drive.call(this);
console.log("Rolling on all " + this.wheels + " wheels!");
},
});
// 另一种混入函数,可能有重写风险
function mixin(sourceObj, targetObj) {
for (var key in sourceObj) {
targetObj[key] = sourceObj[key];
}
return targetObj;
}
var Vehicle = {
// ...
};
// 首先创建一个空对象并把Vehicle 的内容复制进去
var Car = mixin(Vehicle, {});
// 然后把新内容复制到Car 中
mixin(
{
wheels: 4,
drive: function () {
// ...
},
},
Car
);
// “传统的JavaScript 类”Vehicle
function Vehicle() {
this.engines = 1;
}
Vehicle.prototype.ignition = function () {
console.log("Turning on my engine.");
};
Vehicle.prototype.drive = function () {
this.ignition();
console.log("Steering and moving forward!");
};
// “寄生类” Car
function Car() {
// 首先,car 是一个Vehicle
var car = new Vehicle();
// 接着我们对car 进行定制
car.wheels = 4;
// 保存到Vehicle::drive() 的特殊引用
var vehDrive = car.drive;
// 重写Vehicle::drive()
car.drive = function () {
vehDrive.call(this);
console.log("Rolling on all " + this.wheels + " wheels!");
return car;
};
}
var myCar = new Car();
myCar.drive();
// 发动引擎。
// 手握方向盘!
// 全速前进!
虽然这类技术利用了this 的重新绑定功能,但是Something.cool.call( this ) 仍然无法 变成相对(而且更灵活的)引用,所以使用时千万要小心。通常来说,尽量避免使用这样 的结构,以保证代码的整洁和可维护性。
var Something = {
cool: function () {
this.greeting = "Hello World";
this.count = this.count ? this.count + 1 : 1;
},
};
Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1
var Another = {
cool: function () {
// 隐式把Something 混入Another
Something.cool.call(this);
},
};
Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1 (count 不是共享状态)
第一部分 作用域和闭包
第1章 作用域是什么
作用域规则: 当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。
LHS & RHS: 作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对变量进行赋值,那么就会使用LHS 查询;如果目的是获取变量的值,
console.log(a)
就会使用RHS查询。编译与解析过程: JavaScript 引擎首先会在代码执行前对其进行编译,在这个过程中,像
var a = 2
这样的声明会被分解成两个独立的步骤:var a
在其作用域中声明新变量(代码执行前进行)。a = 2
会查询(LHS 查询)变量a
并对其进行赋值。