作用域和闭包
一、编译原理
- 编译器的两种查询方式
1- LHS, 赋值操作 试图找到变量的容器本身(并不关心原本的值是什么)
2- RHS, 取值操作 试图得到某某的值
- 区分查询方式的意义
1- 对未声明的变量进行RHS时无法找到该变量(导致报错)
2- 对未声明变量进行LHS查询时如果找不到目标变量会在全局作用域中创建一个该名称的变量(严格模式下会报错)
二、欺骗词法作用域
- eval
1- 接收一个字符串作为参数,作用域为当前作用域(eval执行时的作用域)
2- 严格模式中eval运行时产生独立的作用域,在其中的变量声明是局部变量
var eval_b = 2;
var eval_str = 'var b = 3'
function eval_foo(str,a) {
eval(str); // 相当于在此处执行 var b = 3;
console.log(a,b); // 输出的b为内部作用域的b
}
eval_foo(eval_str,1);
console.log('----------------- eval');
- with
将代码运行在指定的作用域中, with运行时拥有独立的作用域
var eval_b = 2;
var eval_str = 'var b = 3'
function eval_foo(str,a) {
eval(str); // 相当于在此处执行 var b = 3;
console.log(a,b); // 输出的b为内部作用域的b
}
eval_foo(eval_str,1);
console.log('----------------- eval');
三、块级作用域
- for/if
1- for/if并不会生成独立的作用域,因此无论是在for循环头部定义的变量还是在{}内定义的变量都会挂载在外部作用域上
2- 变量声明应该距离使用的地方越接近越好,并最大限度地本地化
function forValue() {
var forName = 9;
for(forName = 0; forName < 1; forName++) {
// for并未生成独立的作用域, 因此forName2会挂载在外部作用域上
var forName2 = 100;
console.log('for-inner',forName,forName2);
}
// 覆盖了同名变量,for内部定义的变量也会成为当前作用域下的变量
console.log('for-ourter',forName,forName2);
console.log('---块级作用域-for')
}
forValue()
- 会生成块级作用域的语法
1- with()
2- try-catch的catch分句
3- let / const
四、模块
- 核心概念
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 hello(who) {
return 'let me introduce: ' + who;
}
return { // 返回一个对象
hello: hello
};
});
// 取出一个模块
var bar = MyModules.get('bar');
// 定义一个模块,依赖bar模块
MyModules.define('foo',[bar],function() {
})
- 特征
**1- **为创建内部作用域而调用了一个包装函数;
**2- **包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。
this和原型对象
一、this绑定规则
- 默认绑定
直接调用时默认绑定到window上(严格模式中this会绑定到undefined上)
如:seterInterval() ===> window.seterInterval(); // 两者等价
- 隐式绑定
被谁调用this指向谁(受调用链上最后一层调用影响),使用别名调用函数时可能会被绑定到window上
// 隐式丢失问题
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
// "oops, global",在window下调用,、
bar();
// "oops, global",传入函数引用,相当于在window下调用
setTimeout( obj.foo, 0 );
- 显式绑定
call, apply, bind, 一参传null时实际是进行默认绑定到window上;
// 硬绑定, 固定this指向
function foo() {
log(this.a);
}
function bar() {
foo.call(obj); // 利用一个包装函数把作用域绑定到指定对象上
};
var obj = {
a: 2
};
bar(); // 2, 作用域绑定在了固定的对象上
- new绑定
创建新对象 - 绑定原型链 - 绑定this(新对象) - 返回对象; 实际上并没有什么所谓的构造函数,只是对函数的"构造调用"
二、对象
- null的问题
typeof null; // 'object'
原理: 不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判断为 object 类型, null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“ object ”。
- 内置对象
1- 内置对象都是Object的子类型
2- 包括: String, Number, Boolean, Object, Function, Array, Date, RegExp, Error
3- 这些内置对象实际上是一些内置函数,经常被当做构造函数使用
4- 对于Object, Array, Function, RegExp来说, 无论使用字面量还是构造形式,创建出来的都是对象而不是字面量
5- JS中没有类,只有对象
- 基本数据类型和包装类的区别
// 实际上一个字符字面量是无法改变的,重新赋值是销毁后新建
var str = 'i am a str';
typeof str; // 'string'
str instanceof String; // false
var strObj = new String('i am a str');
typeof strObj; // 'object'
strObj instanceof String; // true
三、对象特性
- 属性描述符
var obj = {};
Object.defineProperty(obj,'a',{
value: 2,
writable: true, // 是否可写
configurable: true, // 是否可配置,设置为false后不可逆
enumerable: true // 是否可枚举
});
log(obj);
- 不变性
var obj = {};
try{
// 1.使一个对象成为常量属性(不可修改,重定义或删除)
Object.defineProperty(obj,'a',{
value: 2,
writable: false, // 不可写
configurable: false // 不可配置
})
// 2.禁止对象添加新属性并保留已有属性
Object.preventExtensions(obj);
// 3.密封
// 创建一个密封的对象,这个方法实际上会在目标对象上调用Object.preventExtensions()方法并把所有属性标记为不可配置
Object.seal(obj);
// 4.冻结
// 冻结对象, 这个方法实际上会在一个现有对象上调用Object.seal()方法并把所有属性设置为不可写(依然是浅冻结)
Object.freeze(obj);
// 以上方法都是浅冻结,想要深度冻结一个对象(引用类型也不可改变)需要遍历出所有的对象对其冻结
}catch(e){}
四、属性访问
- [[Get]]和[[Put]]
var getObj = {a:1,c:undefined};
- [[Get]]
getObj.a
;
1, 在语言规范中,getObj.a在getObj上实际上是实现了[[Get]]操作(类似于函数调用[Get])
getObj.b
;
undefined, 对象内置的[[Get]]操作在对象中查找是否有名称相同的属性,有就返回,没有则会去原型链上查找,原型链上没有则返回undefined
getObj.c
;
undefined, c和b的返回值都是undefined但是获取b的值时进行了更复杂的操作- [[Put]] 被触发时大致会检查以下内容:
1.属性是否是访问描述符(是否设置setter/getter)?
2.writable == false?
3.没有上述情况将该值设置为属性的值
4.如果对象中没有这个属性会进行更复杂的操作
- Getter和Setter
1- ES5可以用getter和setter改写默认操作,但是只能应用在单个属性上
2- 给属性定义getter/setter时这个属性会被定义为"访问描述符",js会忽略它们的value和writable特性,取而代之的是set和get特性;
var getterObj = {
a: 1,
b:3,
get a() {
return 2;
}
};
log(getterObj.a); // 2
getterObj.a = 3;
log(getterObj.a); // 2
// 示例
var setterObj = {
get a() {
return this._a_; // 用一个中间变量传递
},
set a(val) {
this._a_ = val * 2;
}
}
- 属性的存在性(属性不存在还是值为undefined)
'a' in setterObj
in操作符会在原型链上查找
setterObj.hasOwnProperty('a')
只会在目标对象上查找
五、原型链
- 关于原型继承和构造函数
1- 原型链的尽头: 普通对象最终指向Object.prototype
2- 所谓原型继承(误): 绑定原型链时并没有完全复制原型链上的属性方法到目标对象,而是在两个对象间建立了连接,目标对象通过委托访问另一个对象(原型链)的属性和函数,所以继承的说法并不严谨
3- 所谓构造函数(误): 实际上JS中并没有什么所谓的构造函数,只是对函数的"构造调用(函数前加上new,new会劫持所有普通函数并用构造对象的形式来调用它)"
function doSomething(name) {
this.name = name;
log('假装自己是构造函数',name);
}
doSomething.prototype.show = function() {
log(this.name);
}
var a = new doSomething('a');
a.show(); // 'a', a上并没有show方法,会通过委托访问到原型链上找
log(a.constructor == doSomething.prototype.constructor); // true, 同上, a.constructor指向一个函数并不是说这是a的构造函数,而是因为对.constructor这个方法的访问被委托到了原型链上!!!
a.__proto__ = Object.create(null); // 把a的原型链指向一个"空"对象
log(a.constructor); // undefined, 证明之前的.constructor操作是被委托到原型链上了
- 判断原型链的关联关系
a instanceof doSomething;
在 a 的整条 [[Prototype]] 链中是否有指向 doSomething.prototype 的对象?
doSomething.prototype.isPrototypeOf(a);
在 a 的整条 [[Prototype]] 链中是否出现过 Foo.prototype
- __proto__的内部实现
try{
throw '__proto__的内部实现';
Object.defineProperty(Object.prototype, "__proto__", {
get: function () {
return Object.getPrototypeOf(this);
},
set: function (o) {
Object.setPrototypeOf(this, o);
return o;
}
});
}catch(e) {log(e);}
- 字典
用Object.create(null)方法创建一个绝对空对象来存储数据,不会受到原型链的干扰,可称之为字典