一、javascript 编译器声明变量过程?【目录: 1.2.2】
[否]声明 -> 查找 -> 赋值
var a = 2;
- 遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的 集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作 用域的集合中声明一个新的变量,并命名为 a。
- 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值 操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的 变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量(查看 1.3 节)。 如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会举手示意并抛出一个异常!
总结: 变量的赋值操作会执行两个动作:1、在当前作用域中声明一个变量(如果之前没有声明过);2、在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值,否则异常
二、欺骗JavaScript词法作用域的两种机制:eval('...')和with(obj){}语句【目录:2.3】
eval可以对一段包 含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在 运行时)。with本质上是通过将一个对象的引用当作作用域来处理,将对象的属性当作作 用域中的标识符来处理,从而创建了一个新的词法作用域(同样是在运行时)
这两个机制的副作用是引擎无法在编译时对作用域查找进行优化,因为引擎只能谨慎地认 为这样的优化是无效的。使用这其中任何一个机制都将导致代码运行变慢。
不要使用它们
。
with本身带的副作用
a = {
m: 1
}
b = {
n: 2
}
function f(obj) {
with(obj) {
m = 3
}
}
f(a);
f(b);
console.log(a.m); // 3
// 变量不存在,引起的副作用
console.log(b.m); // undefined
// 意外副作用引起的内存泄漏
console.log(m); // 3
三、函数作用域 + 块作用域
1)相关概念
1.. 函数作用域的含义: 属于这个函数的全部变量都可以在整个函数的范围内使用及复 用(事实上在嵌套的作用域中也可以使用)。
- 函数声明与函数表达式的区别:①以function开头的就是函数声明,相反则是表达式; ②表达式可以是匿名的 - 匿名表达式,函数声明必须有名字。
// 函数声明
function fn(){}
// 函数表达式
(function fn(){})()
(function(){}())
四、 this全面解析
1. this指向: 每个函数的this是在调用时绑定的,完全取决于调用位置(函数的调用方法)
2.绑定规则:
- 2.1 默认绑定: 独立函数调用 this指向全局对象window
funtion fn() {
console.log(this.a);
}
var a = 1;
fn(); // 1
- 2.2 隐式绑定: 调用位置是否有上下文对象,或者是否被某个对象拥有或者包含。
fuction fn () {
console.log(this.a);
}
var obj = {
a = 1,
fn: fn
}
obj.fn(); // 1
对象属性引用链中只有最顶层或者说最后一层会影响调用位置
:代码如下
fuction fn () {
console.log(this.a);
}
var obj = {
a = 1,
fn: fn
}
var obj2 = {
a = 2,
obj: obj
}
obj2.obj.fn(); // 1
隐示丢失
: 一个最常见的this绑定问题,丢失绑定对象,会被默认绑定将this绑定到全局对象或者undefined上,取决于是否是严格模式。常见方式:如下
function foo() {
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
// a 是全局对象的属性
var a = "oops, global";
doFoo( obj.foo );
// "oops, global
解决方式:固定this
2.3 显示绑定
function foo() {
console.log( this.a );
}
var obj = { a:2 };
foo.call( obj );
// 2 通过 foo.call(..),我们可以在调用 foo 时强制把它的 this 绑定到 obj 上。
可惜,显式绑定仍然无法解决
我们之前提出的丢失绑定
问题。
- 2.3.1 硬绑定: 显式绑定的一个变种可以解决
丢失绑定
这个问题。
function foo() {
console.log( this.a );
}
var obj = { a:2 };
var bar = function() {
foo.call( obj );
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// 硬绑定的 bar 不可能再修改它的 this
bar.call( window ); // 2
我们来看看这个变种到底是怎样工作的。我们创建了函数 bar(),并在它的内部手动调用 了 foo.call(obj),因此强制把 foo 的 this 绑定到了 obj。无论之后如何调用函数 bar,它 总会手动在 obj 上调用 foo。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。
硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = { a:2 };
var bar = function() {
return foo.apply( obj, arguments );
};
var b = bar( 3 ); // 2 3
console.log( b ); // 5
由于硬绑定是一种非常常用的模式,所以在 ES5 中提供了内置的方法 Function.prototype. bind,它的用法如下:
function foo(something) {
console.log( this.a, something );
return this.a + something;
}
var obj = { a:2 };
var bar = foo.bind( obj );
var b = bar( 3 ); // 2 3
console.log( b ); // 5
bind(..) 会返回一个硬编码的新函数,它会把参数设置为 this 的上下文并调用原始函数。
- 2.3.2 API调用的“上下文”: 第三方库函数自带可选参数
第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一 个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保你的回调 函数使用指定的 this。 举例来说:
function foo(el) {
console.log( el, this.id );
}
var obj = { id: "awesome" };// 调用 foo(..) 时把 this 绑定到
obj [1, 2, 3].forEach( foo, obj ); // 1 awesome 2 awesome 3
awesome 这些函数实际上就是通过 call(..) 或者 apply(..) 实现了显式绑定,这样你可以少些一些 代码
2.4 new绑定
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。
(1. 创建(或者说构造)一个全新的对象。
(2. 这个新对象会被执行 [[ 原型 ]] 连接。
(3. 这个新对象会绑定到函数调用的 this。
(4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
我们现在关心的是第 1 步、第 3 步、第 4 步,所以暂时跳过第 2 步,第 5 章会详细介绍它。 思考下面的代码:
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
使用 new 来调用 foo(..) 时,我们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。
3. 优先级: 显式绑定 > 隐式绑定 > 默认绑定
new绑定与硬绑定(bind):默认bind实现,判断硬绑定函数是否是被 new 调用,如果是的话就会使用新创建 的 this 替换硬绑定的 this。
那么,为什么要在 new 中使用硬绑定函数呢?直接使用普通函数不是更简单吗?
之所以要在 new 中使用硬绑定函数,主要目的是预先设置函数的一些参数,这样在使用 new 进行初始化时就可以只传入其余的参数。bind(..) 的功能之一就是可以把除了第一个 参数(第一个参数用于绑定 this)之外的其他参数都传给下层的函数(这种技术称为“部 分应用”,是“柯里化”的一种)。
举例来说:
function foo(p1,p2) {
this.val = p1 + p2;
}
// 之所以使用 null 是因为在本例中我们并不关心硬绑定的 this 是什么
// 反正使用 new 时 this 会被修改
var bar = foo.bind( null, "p1" );
var baz = new bar( "p2" );
baz.val; // p1p2
判断this
现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的 顺序来进行判断:
(1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。 var bar = new foo()
(2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。 var bar = foo.call(obj2)
(3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()
(4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定到 全局对象。
var bar = foo() 就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白 this 的绑定原理了。 不过……凡事总有例外
4. 软绑定
硬绑定这种方式可以把 this 强制绑定到指定的对象(除了使用 new 时),防止函数调用应用默认绑定规则。问题在于,硬绑定会大大降低函数的灵活性,使 用硬绑定之后就无法使用隐式绑定或者显式绑定来修改 this。
如果可以给默认绑定指定一个全局对象和 undefined 以外的值,那就可以实现和硬绑定相 同的效果,同时保留隐式绑定或者显式绑定修改 this 的能力。
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;
};
}
五、对象
1. 对象可以通过两种形式定义:声明形式和构造形式
- 声明形式
var myObj = {}
- 构造形式
var myObj = new Object();
2. 基本类型: string、number、boolean、null、undefined、symbol、object
3. 内置对象: String、Number、Boolean、Object、Function、Array、Date、RegExp、Error
判断数据类型通用函数
let isType = (any) => {
return Object.prototype.toString.call(any).slice(8, -1).toLowerCase();
}
console.log(isType([])); // array
console.log(isType(123)); // number
六、混合对象(类)
继承:子类继承父类属性和方法
多态:子类重写父类属性和方法,继承是多态基础
七、原型[[prototype]]
JavaScript 中的对象有一个特殊的 [[Prototype]] 内置属性,其实就是对于其他对象的引 用。几乎所有的对象在创建时 [[Prototype]] 属性都会被赋予一个非空的值。
注意:很快我们就可以看到,对象的 [[Prototype]] 链接可以为空,虽然很少见: let noting = Object.create(null)
Object.prototype: [[prototype]]的尽头
八、行为委托:
JavaScript 中这个机制的本质就是
对象之间的关联关系
。
- 面向对象风格:
function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function() {
return "I am " + this.me;
};
function Bar(who) {
Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );
b1.speak();
b2.speak();
- 行为委托
Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return "I am " + this.me;
}
};
Bar = Object.create( Foo );
Bar.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak();
这段代码中我们同样利用 [[Prototype]] 把 b1 委托给 Bar 并把 Bar 委托给 Foo,和上一段 代码一模一样。我们仍然实现了三个对象之间的关联。
但是非常重要的一点是,这段代码简洁了许多,我们只是把对象关联起来,并不需要那些 既复杂又令人困惑的模仿类的行为(构造函数、原型以及 new)。