karine

karine 查看完整档案

广州编辑广东外语外贸大学  |  软件工程 编辑  |  填写所在公司/组织 blog.csdn.net/kalinara 编辑
编辑

前端工程师

个人动态

karine 收藏了文章 · 2018-09-11

深入理解 js this 绑定 ( 无需死记硬背,尾部有总结和面试题解析 )

js 的 this 绑定问题,让多数新手懵逼,部分老手觉得恶心,这是因为this的绑定 ‘难以捉摸’,出错的时候还往往不知道为什么,相当反逻辑。
让我们考虑下面代码:

var people = {
    name : "海洋饼干",
    getName : function(){
        console.log(this.name);
    }
};
window.onload = function(){
    xxx.onclick =  people.getName;
};

在平时搬砖时比较常见的this绑定问题,大家可能也写给或者遇到过,当xxx.onclick触发时,输出什么呢 ?

为了方便测试,我将代码简化:

var people = {
    Name: "海洋饼干",
    getName : function(){
        console.log(this.Name);
    }
};
var bar = people.getName;

bar();    // undefined

通过这个小例子带大家感受一下this恶心的地方,我最开始遇到这个问题的时候也是一脸懵逼,因为代码里的this在创建时指向非常明显啊,指向自己 people 对象,但是实际上指向 window 对象,这就是我马上要和大家说的 this 绑定规则

1 . this

什么是this ?在讨论this绑定前,我们得先搞清楚this代表什么。

  1. this是JavaScript的关键字之一。它是 对象 自动生成的一个内部对象,只能在 对象 内部使用。随着函数使用场合的不同,this的值会发生变化。
  2. this指向什么,完全取决于 什么地方以什么方式调用,而不是 创建时。(比较多人误解的地方)(它非常语义化,this在英文中的含义就是 这,这个 ,但这其实起到了一定的误导作用,因为this并不是一成不变的,并不一定一直指向当前 这个

2 . this 绑定规则

掌握了下面介绍的4种绑定的规则,那么你只要看到函数调用就可以判断 this 的指向了

2 .1 默认绑定

考虑下面代码:

function foo(){
    var a = 1 ;
    console.log(this.a);    // 10
}
var a = 10;
foo();

这种就是典型的默认绑定,我们看看foo调用的位置,”光杆司令“,像 这种直接使用而不带任何修饰的函数调用 ,就 默认且只能 应用 默认绑定。

那默认绑定到哪呢,一般是window上,严格模式下 是undefined

2 .2 隐性绑定

代码说话:

function foo(){
    console.log(this.a);
}
var obj = {
    a : 10,
    foo : foo
}
foo();                // ?

obj.foo();            // ?

答案 : undefined 10

foo()的这个写法熟悉吗,就是我们刚刚写的默认绑定,等价于打印window.a,故输出undefined ,
下面obj.foo()这种大家应该经常写,这其实就是我们马上要讨论的 隐性绑定

函数foo执行的时候有了上下文对象,即 obj。这种情况下,函数里的this默认绑定为上下文对象,等价于打印obj.a,故输出10

如果是链性的关系,比如 xx.yy.obj.foo();, 上下文取函数的直接上级,即紧挨着的那个,或者说对象链的最后一个。

2 .3 显性绑定

2 .3 .1 隐性绑定的限制

在我们刚刚的 隐性绑定中有一个致命的限制,就是上下文必须包含我们的函数 ,例:var obj = { foo : foo },如果上下文不包含我们的函数用隐性绑定明显是要出错的,不可能每个对象都要加这个函数 ,那样的话扩展,维护性太差了,我们接下来聊的就是直接 给函数强制性绑定this

2 .3 .2 call apply bind

这里我们就要用到 js 给我们提供的函数 call 和 apply,它们的作用都是改变函数的this指向第一个参数都是 设置this对象

两个函数的区别:

  1. call从第二个参数开始所有的参数都是 原函数的参数。
  2. apply只接受两个参数,且第二个参数必须是数组,这个数组代表原函数的参数列表。

例如:

function foo(a,b){
    console.log(a+b);
}
foo.call(null,'海洋','饼干');        // 海洋饼干  这里this指向不重要就写null了
foo.apply(null, ['海洋','饼干'] );     // 海洋饼干

除了 call,apply函数以外,还有一个改变this的函数 bind ,它和call,apply都不同。

bind只有一个函数,且不会立刻执行,只是将一个值绑定到函数的this上,并将绑定好的函数返回。例:

function foo(){
    console.log(this.a);
}
var obj = { a : 10 };

foo = foo.bind(obj);
foo();                    // 10

(bind函数非常特别,下次和大家一起讨论它的源码)

2 .3 .2 显性绑定

开始正题,上代码,就用上面隐性绑定的例子 :

function foo(){
    console.log(this.a);
}
var obj = {
    a : 10            //去掉里面的foo
}
foo.call(obj);        // 10

我们将隐性绑定例子中的 上下文对象 里的函数去掉了,显然现在不能用 上下文.函数 这种形式来调用函数,大家看代码里的显性绑定代码foo.call(obj),看起来很怪,和我们之前所了解的函数调用不一样。

其实call 是 foo 上的一个函数,在改变this指向的同时执行这个函数。

(想要深入理解 [call apply bind this硬绑定,软绑定,箭头函数绑定 ] 等更多黑科技 的小伙伴欢迎关注我或本文的评论,最近我会单独做一期放到一起写一篇文章)(不想看的小伙伴不用担心,不影响对本文的理解

2 .4 new 绑定

2 .4 .1 什么是 new

学过面向对象的小伙伴对new肯定不陌生,js的new和传统的面向对象语言的new的作用都是创建一个新的对象,但是他们的机制完全不同。

创建一个新对象少不了一个概念,那就是构造函数,传统的面向对象 构造函数 是类里的一种特殊函数,要创建对象时使用new 类名()的形式去调用类中的构造函数,而js中就不一样了。

js中的只要用new修饰的 函数就是'构造函数',准确来说是 函数的构造调用,因为在js中并不存在所谓的'构造函数'。

那么用new 做到函数的构造调用后,js帮我们做了什么工作呢:

  1. 创建一个新对象。
  2. 把这个新对象的__proto__属性指向 原函数的prototype属性。(即继承原函数的原型)
  3. 将这个新对象绑定到 此函数的this上
  4. 返回新对象,如果这个函数没有返回其他对象

第三条就是我们下面要聊的new绑定

2 .4 .2 new 绑定

不哔哔,看代码:

function foo(){
    this.a = 10;
    console.log(this);
}
foo();                    // window对象
console.log(window.a);    // 10   默认绑定

var obj = new foo();      // foo{ a : 10 }  创建的新对象的默认名为函数名
                          // 然后等价于 foo { a : 10 };  var obj = foo;
console.log(obj.a);       // 10    new绑定

使用new调用函数后,函数会 以自己的名字 命名 和 创建 一个新的对象,并返回。

特别注意 : 如果原函数返回一个对象类型,那么将无法返回新对象,你将丢失绑定this的新对象,例:

function foo(){
    this.a = 10;
    return new String("捣蛋鬼");
}
var obj = new foo();
console.log(obj.a);       // undefined
console.log(obj);         // "捣蛋鬼"

2 .5 this绑定优先级

过程是些无聊的代码测试,我直接写出优先级了

new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定

3 . 总结

  1. 如果函数被new 修饰

       this绑定的是新创建的对象,例:var bar = new foo();  函数 foo 中的 this 就是一个叫foo的新创建的对象 , 然后将这个对象赋给bar , 这样的绑定方式叫 new绑定 .
  2. 如果函数是使用call,apply,bind来调用的

       this绑定的是 call,apply,bind 的第一个参数.例: foo.call(obj); , foo 中的 this 就是 obj , 这样的绑定方式叫 显性绑定 .
  3. 如果函数是在某个 上下文对象 下被调用

       this绑定的是那个上下文对象,例 : var obj = { foo : foo };    obj.foo();  foo 中的 this 就是 obj . 这样的绑定方式叫 隐性绑定 .
  4. 如果都不是,即使用默认绑定

       例:function foo(){...} foo() ,foo 中的 this 就是 window.(严格模式下默认绑定到undefined).
       这样的绑定方式叫 默认绑定 .
    

4 . 面试题解析

1.

var x = 10;
var obj = {
    x: 20,
    f: function(){
        console.log(this.x);        // ?
        var foo = function(){ 
            console.log(this.x);    
            }
        foo();                      // ?
    }
};
obj.f();

-----------------------答案---------------------
答案 : 20 10
解析 :考点 1. this默认绑定 2. this隐性绑定

var x = 10;
var obj = {
    x: 20,
    f: function(){
        console.log(this.x);    // 20
                                // 典型的隐性绑定,这里 f 的this指向上下文 obj ,即输出 20
        function foo(){ 
            console.log(this.x); 
            }
        foo();       // 10
                     //有些人在这个地方就想当然的觉得 foo 在函数 f 里,也在 f 里执行,
                     //那 this 肯定是指向obj 啊 , 仔细看看我们说的this绑定规则 , 对应一下很容易
                     //发现这种'光杆司令',是我们一开始就示范的默认绑定,这里this绑定的是window
    }
};
obj.f();             

2.

function foo(arg){
    this.a = arg;
    return this
};

var a = foo(1);
var b = foo(10);

console.log(a.a);    // ?
console.log(b.a);    // ?

-----------------------答案---------------------

答案 : undefined 10
解析 :考点 1. 全局污染 2. this默认绑定

这道题很有意思,问题基本上都集中在第一undefined上,这其实是题目的小陷阱,但是追栈的过程绝对精彩
让我们一步步分析这里发生了什么:

  1. foo(1)执行,应该不难看出是默认绑定吧 , this指向了window,函数里等价于 window.a = 1,return window;
  2. var a = foo(1) 等价于 window.a = window , 很多人都忽略了var a 就是window.a ,将刚刚赋值的 1 替换掉了。
  3. 所以这里的 a 的值是 window , a.a 也是window , 即window.a = window ; window.a.a = window;
  4. foo(10) 和第一次一样,都是默认绑定,这个时候,将window.a 赋值成 10 ,注意这里是关键,原来window.a = window ,现在被赋值成了10,变成了值类型,所以现在 a.a = undefined。(验证这一点只需要将var b = foo(10);删掉,这里的 a.a 还是window)
  5. var b = foo(10); 等价于 window.b = window;

本题中所有变量的值,a = window.a = 10 , a.a = undefined , b = window , b.a = window.a = 10;

3.

var x = 10;
var obj = {
    x: 20,
    f: function(){ console.log(this.x); }
};
var bar = obj.f;
var obj2 = {
    x: 30,
    f: obj.f
}
obj.f();
bar();
obj2.f();

-----------------------答案---------------------
答案:20 10 30
解析:传说中的送分题,考点,辨别this绑定

var x = 10;
var obj = {
    x: 20,
    f: function(){ console.log(this.x); }
};
var bar = obj.f;
var obj2 = {
    x: 30,
    f: obj.f
}
obj.f();    // 20
            //有上下文,this为obj,隐性绑定
bar();      // 10
            //'光杆司令' 默认绑定  ( obj.f 只是普通的赋值操作 )
obj2.f();   //30
            //不管 f 函数怎么折腾,this只和 执行位置和方式有关,即我们所说的绑定规则
            

4. 压轴题了

function foo() {
    getName = function () { console.log (1); };
    return this;
}
foo.getName = function () { console.log(2);};
foo.prototype.getName = function () { console.log(3);};
var getName = function () { console.log(4);};
function getName () { console.log(5);}
 
foo.getName ();                // ?
getName ();                    // ?
foo().getName ();              // ?
getName ();                    // ?
new foo.getName ();            // ?
new foo().getName ();          // ?
new new foo().getName ();      // ?

-----------------------答案---------------------
答案:2 4 1 1 2 3 3
解析:考点 1. new绑定 2.隐性绑定 3. 默认绑定 4.变量污染

function foo() {
    getName = function () { console.log (1); }; 
            //这里的getName 将创建到全局window上
    return this;
}
foo.getName = function () { console.log(2);};   
        //这个getName和上面的不同,是直接添加到foo上的
foo.prototype.getName = function () { console.log(3);}; 
        // 这个getName直接添加到foo的原型上,在用new创建新对象时将直接添加到新对象上 
var getName = function () { console.log(4);}; 
        // 和foo函数里的getName一样, 将创建到全局window上
function getName () { console.log(5);}    
        // 同上,但是这个函数不会被使用,因为函数声明的提升优先级最高,所以上面的函数表达式将永远替换
        // 这个同名函数,除非在函数表达式赋值前去调用getName(),但是在本题中,函数调用都在函数表达式
        // 之后,所以这个函数可以忽略了
        
        // 通过上面对 getName的分析基本上答案已经出来了

foo.getName ();                // 2
                               // 下面为了方便,我就使用输出值来简称每个getName函数
                               // 这里有小伙伴疑惑是在 2 和 3 之间,觉得应该是3 , 但其实直接设置
                               // foo.prototype上的属性,对当前这个对象的属性是没有影响的,如果要使
                               // 用的话,可以foo.prototype.getName() 这样调用 ,这里需要知道的是
                               // 3 并不会覆盖 2,两者不冲突 ( 当你使用new 创建对象时,这里的
                               // Prototype 将自动绑定到新对象上,即用new 构造调用的第二个作用)
                               
getName ();                    // 4 
                               // 这里涉及到函数提升的问题,不知道的小伙伴只需要知道 5 会被 4 覆盖,
                               // 虽然 5 在 4 的下面,其实 js 并不是完全的自上而下,想要深入了解的
                               // 小伙伴可以看文章最后的链接
                               
foo().getName ();              // 1 
                               // 这里的foo函数执行完成了两件事, 1. 将window.getName设置为1,
                               // 2. 返回window , 故等价于 window.getName(); 输出 1
getName ();                    // 1
                               // 刚刚上面的函数刚把window.getName设置为1,故同上 输出 1
                               
new foo.getName ();            // 2
                               // new 对一个函数进行构造调用 , 即 foo.getName ,构造调用也是调用啊
                               // 该执行还是执行,然后返回一个新对象,输出 2 (虽然这里没有接收新
                               // 创建的对象但是我们可以猜到,是一个函数名为 foo.getName 的对象
                               // 且__proto__属性里有一个getName函数,是上面设置的 3 函数)
                               
new foo().getName ();          // 3
                               // 这里特别的地方就来了,new 是对一个函数进行构造调用,它直接找到了离它
                               // 最近的函数,foo(),并返回了应该新对象,等价于 var obj = new foo();
                               // obj.getName(); 这样就很清晰了,输出的是之前绑定到prototype上的
                               // 那个getName  3 ,因为使用new后会将函数的prototype继承给 新对象
                               
new new foo().getName ();      // 3
                               // 哈哈,这个看上去很吓人,让我们来分解一下:
                               // var obj = new foo();
                               // var obj1 = new obj.getName();
                               // 好了,仔细看看, 这不就是上两题的合体吗,obj 有getName 3, 即输出3
                               // obj 是一个函数名为 foo的对象,obj1是一个函数名为obj.getName的对象

5 . 箭头函数的this绑定 (2017.9.18更新)

箭头函数,一种特殊的函数,不使用function关键字,而是使用=>,学名 胖箭头(2333),它和普通函数的区别:

  1. 箭头函数不使用我们上面介绍的四种绑定,而是完全根据外部作用域来决定this。(它的父级是使用我们的规则的哦)
  2. 箭头函数的this绑定无法被修改 (这个特性非常爽(滑稽))

先看个代码巩固一下:

function foo(){
    return ()=>{
        console.log(this.a);
    }
}
foo.a = 10;

// 1. 箭头函数关联父级作用域this

var bar = foo();            // foo默认绑定
bar();                      // undefined 哈哈,是不是有小伙伴想当然了

var baz = foo.call(foo);    // foo 显性绑定
baz();                      // 10 

// 2. 箭头函数this不可修改
//这里我们使用上面的已经绑定了foo 的 baz
var obj = {
    a : 999
}
baz.call(obj);              // 10

来来来,实战一下,还记得我们之前第一个例子吗,将它改成箭头函数的形式(可以彻底解决恶心的this绑定问题):

var people = {
    Name: "海洋饼干",
    getName : function(){
        console.log(this.Name);
    }
};
var bar = people.getName;

bar();    // undefined

====================修改后====================

var people = {
    Name: "海洋饼干",
    getName : function(){
        return ()=>{
            console.log(this.Name);
        }
    }
};
var bar = people.getName(); //获得一个永远指向people的函数,不用想this了,岂不是美滋滋?

bar();    // 海洋饼干 

可能会有人不解为什么在箭头函数外面再套一层,直接写不就行了吗,搞这么麻烦干嘛,其实这也是箭头函数很多人用不好的地方

var obj= {
    that : this,
    bar : function(){
        return ()=>{
            console.log(this);
        }
    },
    baz : ()=>{
        console.log(this);
    }
}
console.log(obj.that);  // window
obj.bar()();            // obj
obj.baz();              // window
  1. 我们先要搞清楚一点,obj的当前作用域是window,如 obj.that === window。
  2. 如果不用function(function有自己的函数作用域)将其包裹起来,那么默认绑定的父级作用域就是window。
  3. 用function包裹的目的就是将箭头函数绑定到当前的对象上。函数的作用域是当前这个对象,然后箭头函数会自动绑定函数所在作用域的this,即obj。

美滋滋,溜了溜了




参考书籍:你不知道的JavaScript<上卷> KYLE SIMPSON 著 (推荐)

查看原文

karine 赞了文章 · 2018-09-11

深入理解 js this 绑定 ( 无需死记硬背,尾部有总结和面试题解析 )

js 的 this 绑定问题,让多数新手懵逼,部分老手觉得恶心,这是因为this的绑定 ‘难以捉摸’,出错的时候还往往不知道为什么,相当反逻辑。
让我们考虑下面代码:

var people = {
    name : "海洋饼干",
    getName : function(){
        console.log(this.name);
    }
};
window.onload = function(){
    xxx.onclick =  people.getName;
};

在平时搬砖时比较常见的this绑定问题,大家可能也写给或者遇到过,当xxx.onclick触发时,输出什么呢 ?

为了方便测试,我将代码简化:

var people = {
    Name: "海洋饼干",
    getName : function(){
        console.log(this.Name);
    }
};
var bar = people.getName;

bar();    // undefined

通过这个小例子带大家感受一下this恶心的地方,我最开始遇到这个问题的时候也是一脸懵逼,因为代码里的this在创建时指向非常明显啊,指向自己 people 对象,但是实际上指向 window 对象,这就是我马上要和大家说的 this 绑定规则

1 . this

什么是this ?在讨论this绑定前,我们得先搞清楚this代表什么。

  1. this是JavaScript的关键字之一。它是 对象 自动生成的一个内部对象,只能在 对象 内部使用。随着函数使用场合的不同,this的值会发生变化。
  2. this指向什么,完全取决于 什么地方以什么方式调用,而不是 创建时。(比较多人误解的地方)(它非常语义化,this在英文中的含义就是 这,这个 ,但这其实起到了一定的误导作用,因为this并不是一成不变的,并不一定一直指向当前 这个

2 . this 绑定规则

掌握了下面介绍的4种绑定的规则,那么你只要看到函数调用就可以判断 this 的指向了

2 .1 默认绑定

考虑下面代码:

function foo(){
    var a = 1 ;
    console.log(this.a);    // 10
}
var a = 10;
foo();

这种就是典型的默认绑定,我们看看foo调用的位置,”光杆司令“,像 这种直接使用而不带任何修饰的函数调用 ,就 默认且只能 应用 默认绑定。

那默认绑定到哪呢,一般是window上,严格模式下 是undefined

2 .2 隐性绑定

代码说话:

function foo(){
    console.log(this.a);
}
var obj = {
    a : 10,
    foo : foo
}
foo();                // ?

obj.foo();            // ?

答案 : undefined 10

foo()的这个写法熟悉吗,就是我们刚刚写的默认绑定,等价于打印window.a,故输出undefined ,
下面obj.foo()这种大家应该经常写,这其实就是我们马上要讨论的 隐性绑定

函数foo执行的时候有了上下文对象,即 obj。这种情况下,函数里的this默认绑定为上下文对象,等价于打印obj.a,故输出10

如果是链性的关系,比如 xx.yy.obj.foo();, 上下文取函数的直接上级,即紧挨着的那个,或者说对象链的最后一个。

2 .3 显性绑定

2 .3 .1 隐性绑定的限制

在我们刚刚的 隐性绑定中有一个致命的限制,就是上下文必须包含我们的函数 ,例:var obj = { foo : foo },如果上下文不包含我们的函数用隐性绑定明显是要出错的,不可能每个对象都要加这个函数 ,那样的话扩展,维护性太差了,我们接下来聊的就是直接 给函数强制性绑定this

2 .3 .2 call apply bind

这里我们就要用到 js 给我们提供的函数 call 和 apply,它们的作用都是改变函数的this指向第一个参数都是 设置this对象

两个函数的区别:

  1. call从第二个参数开始所有的参数都是 原函数的参数。
  2. apply只接受两个参数,且第二个参数必须是数组,这个数组代表原函数的参数列表。

例如:

function foo(a,b){
    console.log(a+b);
}
foo.call(null,'海洋','饼干');        // 海洋饼干  这里this指向不重要就写null了
foo.apply(null, ['海洋','饼干'] );     // 海洋饼干

除了 call,apply函数以外,还有一个改变this的函数 bind ,它和call,apply都不同。

bind只有一个函数,且不会立刻执行,只是将一个值绑定到函数的this上,并将绑定好的函数返回。例:

function foo(){
    console.log(this.a);
}
var obj = { a : 10 };

foo = foo.bind(obj);
foo();                    // 10

(bind函数非常特别,下次和大家一起讨论它的源码)

2 .3 .2 显性绑定

开始正题,上代码,就用上面隐性绑定的例子 :

function foo(){
    console.log(this.a);
}
var obj = {
    a : 10            //去掉里面的foo
}
foo.call(obj);        // 10

我们将隐性绑定例子中的 上下文对象 里的函数去掉了,显然现在不能用 上下文.函数 这种形式来调用函数,大家看代码里的显性绑定代码foo.call(obj),看起来很怪,和我们之前所了解的函数调用不一样。

其实call 是 foo 上的一个函数,在改变this指向的同时执行这个函数。

(想要深入理解 [call apply bind this硬绑定,软绑定,箭头函数绑定 ] 等更多黑科技 的小伙伴欢迎关注我或本文的评论,最近我会单独做一期放到一起写一篇文章)(不想看的小伙伴不用担心,不影响对本文的理解

2 .4 new 绑定

2 .4 .1 什么是 new

学过面向对象的小伙伴对new肯定不陌生,js的new和传统的面向对象语言的new的作用都是创建一个新的对象,但是他们的机制完全不同。

创建一个新对象少不了一个概念,那就是构造函数,传统的面向对象 构造函数 是类里的一种特殊函数,要创建对象时使用new 类名()的形式去调用类中的构造函数,而js中就不一样了。

js中的只要用new修饰的 函数就是'构造函数',准确来说是 函数的构造调用,因为在js中并不存在所谓的'构造函数'。

那么用new 做到函数的构造调用后,js帮我们做了什么工作呢:

  1. 创建一个新对象。
  2. 把这个新对象的__proto__属性指向 原函数的prototype属性。(即继承原函数的原型)
  3. 将这个新对象绑定到 此函数的this上
  4. 返回新对象,如果这个函数没有返回其他对象

第三条就是我们下面要聊的new绑定

2 .4 .2 new 绑定

不哔哔,看代码:

function foo(){
    this.a = 10;
    console.log(this);
}
foo();                    // window对象
console.log(window.a);    // 10   默认绑定

var obj = new foo();      // foo{ a : 10 }  创建的新对象的默认名为函数名
                          // 然后等价于 foo { a : 10 };  var obj = foo;
console.log(obj.a);       // 10    new绑定

使用new调用函数后,函数会 以自己的名字 命名 和 创建 一个新的对象,并返回。

特别注意 : 如果原函数返回一个对象类型,那么将无法返回新对象,你将丢失绑定this的新对象,例:

function foo(){
    this.a = 10;
    return new String("捣蛋鬼");
}
var obj = new foo();
console.log(obj.a);       // undefined
console.log(obj);         // "捣蛋鬼"

2 .5 this绑定优先级

过程是些无聊的代码测试,我直接写出优先级了

new 绑定 > 显示绑定 > 隐式绑定 > 默认绑定

3 . 总结

  1. 如果函数被new 修饰

       this绑定的是新创建的对象,例:var bar = new foo();  函数 foo 中的 this 就是一个叫foo的新创建的对象 , 然后将这个对象赋给bar , 这样的绑定方式叫 new绑定 .
  2. 如果函数是使用call,apply,bind来调用的

       this绑定的是 call,apply,bind 的第一个参数.例: foo.call(obj); , foo 中的 this 就是 obj , 这样的绑定方式叫 显性绑定 .
  3. 如果函数是在某个 上下文对象 下被调用

       this绑定的是那个上下文对象,例 : var obj = { foo : foo };    obj.foo();  foo 中的 this 就是 obj . 这样的绑定方式叫 隐性绑定 .
  4. 如果都不是,即使用默认绑定

       例:function foo(){...} foo() ,foo 中的 this 就是 window.(严格模式下默认绑定到undefined).
       这样的绑定方式叫 默认绑定 .
    

4 . 面试题解析

1.

var x = 10;
var obj = {
    x: 20,
    f: function(){
        console.log(this.x);        // ?
        var foo = function(){ 
            console.log(this.x);    
            }
        foo();                      // ?
    }
};
obj.f();

-----------------------答案---------------------
答案 : 20 10
解析 :考点 1. this默认绑定 2. this隐性绑定

var x = 10;
var obj = {
    x: 20,
    f: function(){
        console.log(this.x);    // 20
                                // 典型的隐性绑定,这里 f 的this指向上下文 obj ,即输出 20
        function foo(){ 
            console.log(this.x); 
            }
        foo();       // 10
                     //有些人在这个地方就想当然的觉得 foo 在函数 f 里,也在 f 里执行,
                     //那 this 肯定是指向obj 啊 , 仔细看看我们说的this绑定规则 , 对应一下很容易
                     //发现这种'光杆司令',是我们一开始就示范的默认绑定,这里this绑定的是window
    }
};
obj.f();             

2.

function foo(arg){
    this.a = arg;
    return this
};

var a = foo(1);
var b = foo(10);

console.log(a.a);    // ?
console.log(b.a);    // ?

-----------------------答案---------------------

答案 : undefined 10
解析 :考点 1. 全局污染 2. this默认绑定

这道题很有意思,问题基本上都集中在第一undefined上,这其实是题目的小陷阱,但是追栈的过程绝对精彩
让我们一步步分析这里发生了什么:

  1. foo(1)执行,应该不难看出是默认绑定吧 , this指向了window,函数里等价于 window.a = 1,return window;
  2. var a = foo(1) 等价于 window.a = window , 很多人都忽略了var a 就是window.a ,将刚刚赋值的 1 替换掉了。
  3. 所以这里的 a 的值是 window , a.a 也是window , 即window.a = window ; window.a.a = window;
  4. foo(10) 和第一次一样,都是默认绑定,这个时候,将window.a 赋值成 10 ,注意这里是关键,原来window.a = window ,现在被赋值成了10,变成了值类型,所以现在 a.a = undefined。(验证这一点只需要将var b = foo(10);删掉,这里的 a.a 还是window)
  5. var b = foo(10); 等价于 window.b = window;

本题中所有变量的值,a = window.a = 10 , a.a = undefined , b = window , b.a = window.a = 10;

3.

var x = 10;
var obj = {
    x: 20,
    f: function(){ console.log(this.x); }
};
var bar = obj.f;
var obj2 = {
    x: 30,
    f: obj.f
}
obj.f();
bar();
obj2.f();

-----------------------答案---------------------
答案:20 10 30
解析:传说中的送分题,考点,辨别this绑定

var x = 10;
var obj = {
    x: 20,
    f: function(){ console.log(this.x); }
};
var bar = obj.f;
var obj2 = {
    x: 30,
    f: obj.f
}
obj.f();    // 20
            //有上下文,this为obj,隐性绑定
bar();      // 10
            //'光杆司令' 默认绑定  ( obj.f 只是普通的赋值操作 )
obj2.f();   //30
            //不管 f 函数怎么折腾,this只和 执行位置和方式有关,即我们所说的绑定规则
            

4. 压轴题了

function foo() {
    getName = function () { console.log (1); };
    return this;
}
foo.getName = function () { console.log(2);};
foo.prototype.getName = function () { console.log(3);};
var getName = function () { console.log(4);};
function getName () { console.log(5);}
 
foo.getName ();                // ?
getName ();                    // ?
foo().getName ();              // ?
getName ();                    // ?
new foo.getName ();            // ?
new foo().getName ();          // ?
new new foo().getName ();      // ?

-----------------------答案---------------------
答案:2 4 1 1 2 3 3
解析:考点 1. new绑定 2.隐性绑定 3. 默认绑定 4.变量污染

function foo() {
    getName = function () { console.log (1); }; 
            //这里的getName 将创建到全局window上
    return this;
}
foo.getName = function () { console.log(2);};   
        //这个getName和上面的不同,是直接添加到foo上的
foo.prototype.getName = function () { console.log(3);}; 
        // 这个getName直接添加到foo的原型上,在用new创建新对象时将直接添加到新对象上 
var getName = function () { console.log(4);}; 
        // 和foo函数里的getName一样, 将创建到全局window上
function getName () { console.log(5);}    
        // 同上,但是这个函数不会被使用,因为函数声明的提升优先级最高,所以上面的函数表达式将永远替换
        // 这个同名函数,除非在函数表达式赋值前去调用getName(),但是在本题中,函数调用都在函数表达式
        // 之后,所以这个函数可以忽略了
        
        // 通过上面对 getName的分析基本上答案已经出来了

foo.getName ();                // 2
                               // 下面为了方便,我就使用输出值来简称每个getName函数
                               // 这里有小伙伴疑惑是在 2 和 3 之间,觉得应该是3 , 但其实直接设置
                               // foo.prototype上的属性,对当前这个对象的属性是没有影响的,如果要使
                               // 用的话,可以foo.prototype.getName() 这样调用 ,这里需要知道的是
                               // 3 并不会覆盖 2,两者不冲突 ( 当你使用new 创建对象时,这里的
                               // Prototype 将自动绑定到新对象上,即用new 构造调用的第二个作用)
                               
getName ();                    // 4 
                               // 这里涉及到函数提升的问题,不知道的小伙伴只需要知道 5 会被 4 覆盖,
                               // 虽然 5 在 4 的下面,其实 js 并不是完全的自上而下,想要深入了解的
                               // 小伙伴可以看文章最后的链接
                               
foo().getName ();              // 1 
                               // 这里的foo函数执行完成了两件事, 1. 将window.getName设置为1,
                               // 2. 返回window , 故等价于 window.getName(); 输出 1
getName ();                    // 1
                               // 刚刚上面的函数刚把window.getName设置为1,故同上 输出 1
                               
new foo.getName ();            // 2
                               // new 对一个函数进行构造调用 , 即 foo.getName ,构造调用也是调用啊
                               // 该执行还是执行,然后返回一个新对象,输出 2 (虽然这里没有接收新
                               // 创建的对象但是我们可以猜到,是一个函数名为 foo.getName 的对象
                               // 且__proto__属性里有一个getName函数,是上面设置的 3 函数)
                               
new foo().getName ();          // 3
                               // 这里特别的地方就来了,new 是对一个函数进行构造调用,它直接找到了离它
                               // 最近的函数,foo(),并返回了应该新对象,等价于 var obj = new foo();
                               // obj.getName(); 这样就很清晰了,输出的是之前绑定到prototype上的
                               // 那个getName  3 ,因为使用new后会将函数的prototype继承给 新对象
                               
new new foo().getName ();      // 3
                               // 哈哈,这个看上去很吓人,让我们来分解一下:
                               // var obj = new foo();
                               // var obj1 = new obj.getName();
                               // 好了,仔细看看, 这不就是上两题的合体吗,obj 有getName 3, 即输出3
                               // obj 是一个函数名为 foo的对象,obj1是一个函数名为obj.getName的对象

5 . 箭头函数的this绑定 (2017.9.18更新)

箭头函数,一种特殊的函数,不使用function关键字,而是使用=>,学名 胖箭头(2333),它和普通函数的区别:

  1. 箭头函数不使用我们上面介绍的四种绑定,而是完全根据外部作用域来决定this。(它的父级是使用我们的规则的哦)
  2. 箭头函数的this绑定无法被修改 (这个特性非常爽(滑稽))

先看个代码巩固一下:

function foo(){
    return ()=>{
        console.log(this.a);
    }
}
foo.a = 10;

// 1. 箭头函数关联父级作用域this

var bar = foo();            // foo默认绑定
bar();                      // undefined 哈哈,是不是有小伙伴想当然了

var baz = foo.call(foo);    // foo 显性绑定
baz();                      // 10 

// 2. 箭头函数this不可修改
//这里我们使用上面的已经绑定了foo 的 baz
var obj = {
    a : 999
}
baz.call(obj);              // 10

来来来,实战一下,还记得我们之前第一个例子吗,将它改成箭头函数的形式(可以彻底解决恶心的this绑定问题):

var people = {
    Name: "海洋饼干",
    getName : function(){
        console.log(this.Name);
    }
};
var bar = people.getName;

bar();    // undefined

====================修改后====================

var people = {
    Name: "海洋饼干",
    getName : function(){
        return ()=>{
            console.log(this.Name);
        }
    }
};
var bar = people.getName(); //获得一个永远指向people的函数,不用想this了,岂不是美滋滋?

bar();    // 海洋饼干 

可能会有人不解为什么在箭头函数外面再套一层,直接写不就行了吗,搞这么麻烦干嘛,其实这也是箭头函数很多人用不好的地方

var obj= {
    that : this,
    bar : function(){
        return ()=>{
            console.log(this);
        }
    },
    baz : ()=>{
        console.log(this);
    }
}
console.log(obj.that);  // window
obj.bar()();            // obj
obj.baz();              // window
  1. 我们先要搞清楚一点,obj的当前作用域是window,如 obj.that === window。
  2. 如果不用function(function有自己的函数作用域)将其包裹起来,那么默认绑定的父级作用域就是window。
  3. 用function包裹的目的就是将箭头函数绑定到当前的对象上。函数的作用域是当前这个对象,然后箭头函数会自动绑定函数所在作用域的this,即obj。

美滋滋,溜了溜了




参考书籍:你不知道的JavaScript<上卷> KYLE SIMPSON 著 (推荐)

查看原文

赞 128 收藏 337 评论 49

karine 回答了问题 · 2018-08-24

解决element-ui table 里面的checkbox 怎么实现键盘事件 shift多选?

感谢解答,现在解决了,我的做法是在mounted里分别对keydown和keyup进行addEventListener,如果keycode是16,那就给isShift变量进行true和false的变更,然后watch监听isShift变量,在keyup事件触发后,求min,max,然后在[min,max]这个区间循环调用表格的 toggleRowSelection 事件,大概思路是这样。还有一些更细的情况也要考虑进去,这里就不展开了。

关注 3 回答 3

karine 关注了问题 · 2018-08-23

解决element-ui table 里面的checkbox 怎么实现键盘事件 shift多选?

怎么在el-table 里面实现 shift多选 checkbox

关注 3 回答 3

karine 关注了问题 · 2018-08-23

解决element-ui table 里面的checkbox 怎么实现键盘事件 shift多选?

怎么在el-table 里面实现 shift多选 checkbox

关注 3 回答 3

karine 提出了问题 · 2018-08-23

解决element-ui table 里面的checkbox 怎么实现键盘事件 shift多选?

怎么在el-table 里面实现 shift多选 checkbox

关注 3 回答 3

karine 关注了标签 · 2018-08-23

element-ui

Element,一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库

A Vue.js 2.0 UI Toolkit for Web

https://element.eleme.cn/
https://github.com/ElemeFE/el...

关注 662

karine 关注了问题 · 2018-08-23

Element的el-table 如何响应键盘事件

Hey guys,我想使用敲击空格el-table里实现类似Mac里的QuickLook.
思路是侦听自己的键盘事件,阻止事件继续传播,从而干掉默认敲击空格,滚动条翻页的键盘事件.
但是不知道如何使用el-table侦听键盘事件.
请问该如何实现?

关注 3 回答 1

karine 收藏了文章 · 2018-07-03

[译]关于vertical-align:你需要知道的一切

原文:Vertical-Align: All You Need To Know

通常我都有需要垂直对齐在一排上一个接着一个的元素。
CSS提供了很多种可能性。有时,我听过float来解决问题,有时使用position:absolute,有时甚至会通过添加margin或者padding属性这种使代码变得比较脏的方式来达到目的。

我一点也不喜欢上面这些解决方案。浮动仅仅是顶部对齐并且需要手动清除浮动。绝对定位让元素脱离文档流,所以他们不再影响他们的周边元素。而使用固定值的外边距和内边距会出现细微的差别。

但是这里还有另外一种玩法:vertical-align。我认为他值得更多的赞誉。OK,技术上来说使用vertical-align布局是一种hack的手段,因为他并不是因为实现我们上面那种需求发明的。尽管如此,你也可以在不同的上下文中使用vertical-align来灵活细致的摆放元素。不需要知道元素的大小。元素在文档流中,也能感知其他元素尺寸的改变。这些都让vertical-align变成了更有价值的选项。

独特的vertical-align

但是,vertical-align有时也是个卑鄙小人。使用它工作也会使人有点沮丧。在使用它工作时,这里会有一些令人难以理解的规则。例如,它可能会发生这样的情况,一个元素改变了他自己的vertical-align的值,而他的位置却一点都没有改变,改变的却是同行的其他元素。我仍然会一次一次被拉入这种奇怪的现象,使我苦恼。

不幸的是,大多数资料都讲述的比较浅显。特别是,当我们想使用vertical-align布局时。他们对最基本的属性进行了介绍并且在简单的例子中解释元素是如何在一行中进行摆放的。但是,他们并没有去解释它令人感到奇怪的部分。

所以,我的目标是使自己彻底弄清楚vertical-align到底是怎么回事。最后消解我疑问的是W3C's的CSS specifications和标准中演示的一些例子。答案就在文章中。

所以,让我们来解决掉这场关于规则的游戏吧。

使用vertical-align的要求

vertical-align被用于inline-level elements。这些元素的display属性如下

  • inline

  • inline-block

  • inline-table(这篇文章中,并不考虑这种情况)

基本的inline元素都是标签裹着文字。

inline-block元素:是在行内中的块级元素。他们可以有宽度和高度(通常情况下,这取决于他们的内容)。同样也有padding,margin,border。

行内级元素在一行中一个挨着一个。一旦,这些元素超出了他们的所在行,一个新行便会建立在它下方。这里的每一行就叫做line box。每一行不同尺寸的元素意味着line box不同的高度。在下面的例子中红线代表line box的顶部和底部。

line box寻找我们放置元素的轨迹。在这些line boxes里面vertical-align属性负责摆放单独的元素。所以什么样的元素被用来对齐了呢?

关于基线和边界线

在一行中垂直摆放元素,最关键的参考点是参与的元素的基线。在一些例子中,被关闭盒子的顶部和底部也变得非常重要。让我们一起看看基线和外边缘是如何参与到不同类型的元素中的。

Inline Element

clipboard.png

这里有三行挨着的文字。顶部和底部的边缘线是行高的边缘线,它们都是红线。字体的高度被绿线包裹着。蓝色的线代表基线。在最左边,文字的行高和字体本身的高度一样高。绿线和红线发生了重叠。在中间,行高是字体大小的2倍高。在最右边,行高是字体大小的一半高。

inline element的外边缘的位置,取决于他们的行高的顶部与底部边缘。如果行高的高度比字体的高度小。那么,红色的外边缘的位置就如上面提到的一样。

inline element的基线,在字符放置的位置。在图中,用蓝线表示。初略的讲,基线的位置一般是在字体高度一半以下的位置。可以看看W3C的标准对它的定义,detailed definition

Inline-Block Element

clipboard.png

从左边到右边的图你可以看到:最左,一个在in-flow内的行内块级元素。中间,一个在文档流中的inline-block元素并且带有overflow: hidden属性的元素。最右,不在文档流中的inline-block元素(但是内容区域有高度)。margin的边界线指向红线,border是黄色的部分,padding是绿色的部分,内容区域是蓝色的部分。每一个inline-block元素的基线都用蓝线表示。

inline-block元素的外边缘 是它的margin-box的顶部和底部。在图上,用红线来表示。

inline-block元素的基线 依赖于元素是否是文档流中的元素。

  • 假设内容在文档流中,inline-block元素的基线是普通流中的最后一个内容元素的基线(最左的例子)。对于最后一个元素的基线根据它自己的规则来找寻。

  • 假设内容在文档流中,但是元素有overflow属性除了属性值为visible这种情况,基线在margin-box的底部(最中间的例子)。所以,这等同于在inline-block元素的底部边缘。

  • 假设内容不在文档流中,同样的,margin-box的底部边缘也是基线的位置所在。(例子在最右边)

Line Box

clipboard.png

你已经看到了上面元素的放置。这次,我画了line box的text box顶部和底部的线(如图中的绿线,更多内容看下文),基线如图中蓝色的线。我在文字元素上加了灰色的背景进行强调。

line box的顶部边缘与最顶部元素的顶线对齐,底部边缘与最底部元素的底线对齐。图中红线代表的就是line box。

line box的基线是多种多样的

“CSS2.1 并没有对line box基线的位置有明确的定义。-W3C Specs

当使用vertical-align工作时,这可能是最令人迷惑的部分。这意味着,基线的位置改变是为了满足像垂直对齐和降低line box的高度这样的需求的。也就是说,在方程中,这是一个自由的参数。

因为line box的基线是不可见的,因此它的位置并不会那么明显。但是,你可以容易的使它变得可见。只需要在一行的开头加上一个字母,就像我在图中添加的字母"x"。如果这个字母并没排在一条直线上,那么x坐落的位置就是基线的位置。

在line box基线的周围有text box。text box可以简单的被认为是在line box中的行内元素。它的高度等同于它的父元素的文字大小。因此,text box仅仅只是围绕着line box的非格式化文字。这个盒子在上图中是指向绿线的位置。text box与基线的位置相关联,它随着基线而移动。

哦,现在是最难的部分。现在,我们把知道的所有事,都表现在了上图中。让我们快速的总结最重要的部分:

  • 这里有一个区域被叫做line box。这是元素垂直对齐的区域。它有一个基线和一个text box以及顶部与底部边缘。

  • 这里有inline-level元素。他们是被对齐的对象。他们有基线以及顶部与底部边缘。

Vertical-Align的值

通过使用vertical-align的参考点,也就是上文提到的基线和边界线,将元素放置在合适的位置。

相对于line box的基线放置元素的基线

clipboard.png

  • baseline: 元素的基线刚好放置在line box基线的上。

  • sub: 元素的基线放置在line box基线的下面。

  • super: 元素的基线放置在line box的基线的上面。

  • <percentage> 元素的基线移动相对于line box的基线通过相对于line-height的百分比的值来移动。

  • <length>: 元素的基线相对于line box的基线通过绝对值的大小进行移动。

相对于line box的基线放置元素的外边缘

clipboard.png

  • middle: 元素的顶部与底部边缘的中点相对于line box的基线移动x-height的一半的位置对齐。

相对于line box的text box放置元素的外边缘

clipboard.png

  • text-top: 元素的顶部边缘被放置在line box的text box的顶部边缘

  • text-bottom: 元素的底部边缘被放置在line box的text box的底部边缘

相对于line box的外边缘放置元素的外边缘

clipboard.png

  • top: 元素的顶部边缘被放置在line box的顶部边缘

  • bottom: 元素的底部边缘被放置在line box的底部边缘

在W3C标准中的定义

Vertical-Align的行为就如它的表现一样

现在,我们可以在具体的例子中看垂直对齐。特别是,处理一些比较容易出现差错的情况。

放置icon在中间

这是一个一直以来都令我烦恼的事情:我有一个icon我想将它放置在一行文字的中间。只只仅仅将icon设置vertical-align: middle,看起来似乎不是一个安全的方法。看下面的例子。

clipboard.png

<!-- left mark-up -->
<span class="icon middle"></span>
Centered?

<!-- right mark-up -->
<span class="icon middle"></span>
<span class="middle">Centered!</span>

<style type="text/css">
  .icon   { display: inline-block;
            /* size, color, etc. */ }

  .middle { vertical-align: middle; }
</style>

这里也是上面这个例子,不过,这次我加了一些你从上文了解到的辅助线,

clipboard.png

这有助于帮我们理解。因为在左边的文字并没有对齐元素,而是被放置在基线所处的位置。通过使用vertical-align:middle来对齐盒子,我们将只是将盒子放置在了小写字母的中间。所以,出头部分的字母出现在顶部。

在右边,我们将全部文字区域放置在垂直方向的中点。通过移动text的基线到line box的基线的下方来实现。结果是文字完美的居中于紧挨着的icon。

移动line box的基线

当我们使用vertical-align工作时,容易遇到的一个共同的问题:line box的基线会被在同一行中所有元素影响。让我们猜一猜,一个元素有可能在这样被对齐的,通过移动line box的基线。因为大多数垂直对齐(除了顶部和底部)都是相对于基线,这也会导致在同一行的的所有元素的位置都会被调整。

一些例子:

  • 如果在一行中,有一个高的元素占据了整行的高度,vertical-align对它不会有任何影响。在它的顶部上面与底部下面没有任何多余的空间可以移动它。为了实现它相对于line box基线的对齐,line box的基线必须移动。矮一点的盒子设置了vertical-align: baseline。在左边,高盒子根据text-bottom对齐。在右边,根据text-top对齐。你可以看到矮盒子的随着基线位置的改变而改变。

clipboard.png

<!-- left mark-up -->
<span class="tall-box text-bottom"></span>
<span class="short-box"></span>

<!-- right mark-up -->
<span class="tall-box text-top"></span>
<span class="short-box"></span>

<style type="text/css">
  .tall-box,
  .short-box   { display: inline-block;
                /* size, color, etc. */ }

  .text-bottom { vertical-align: text-bottom; }
  .text-top    { vertical-align: text-top; }
</style>

通过其他vertical-align的值对齐高的元素,也会出现和上面的例子相同的行为。

  • 同样的给它设置vertical-align的值分别设为leftbottom移动基线。而这很奇怪,因为基线本不应该参与。

<!-- left mark-up -->
<span class="tall-box bottom"></span>
<span class="short-box"></span>

<!-- right mark-up -->
<span class="tall-box top"></span>
<span class="short-box"></span>

<style type="text/css">
  .tall-box,
  .short-box { display: inline-block;
               /* size, color, etc. */ }

  .bottom    { vertical-align: bottom; }
  .top       { vertical-align: top; }
</style>

clipboard.png

<!-- left mark-up -->
<span class="tall-box bottom"></span>
<span class="short-box"></span>

<!-- right mark-up -->
<span class="tall-box top"></span>
<span class="short-box"></span>

<style type="text/css">
  .tall-box,
  .short-box { display: inline-block;
               /* size, color, etc. */ }

  .bottom    { vertical-align: bottom; }
  .top       { vertical-align: top; }
</style>
  • 在一行中,放置两个大体积元素并且移动基线垂直对齐他们以此满足同时对齐。line box的高度被调整(左边的例子)。添加第三个元素,它并有跑到line box的边缘因为它的对齐属性,既没有影响line box的高度,也没有影响line box基线的位置(中间的例子)。如果它真的跑到了line box的边缘,我们的前两个盒子会被往下推(最右的例子)。

clipboard.png

<!-- left mark-up -->
<span class="tall-box text-bottom"></span>
<span class="tall-box text-top"></span>

<!-- mark-up in the middle -->
<span class="tall-box text-bottom"></span>
<span class="tall-box text-top"></span>
<span class="tall-box middle"></span>

<!-- right mark-up -->
<span class="tall-box text-bottom"></span>
<span class="tall-box text-top"></span>
<span class="tall-box text-100up"></span>

<style type="text/css">
  .tall-box    { display: inline-block;
                 /* size, color, etc. */ }

  .middle      { vertical-align: middle; }
  .text-top    { vertical-align: text-top; }
  .text-bottom { vertical-align: text-bottom; }
  .text-100up  { vertical-align: 100%; }
</style>

解开Vertical-Align的神秘面纱

当你知道了规则,这就没那么复杂了。如果vertical-align并没有按照你想的那样行动,问自己两个问题:

  • line box的顶部与底部边缘和基线在哪里?

  • inline-level元素的顶部与底部边缘和基线在哪里?

这将解决你的问题。

查看原文

karine 赞了文章 · 2018-07-03

[译]关于vertical-align:你需要知道的一切

原文:Vertical-Align: All You Need To Know

通常我都有需要垂直对齐在一排上一个接着一个的元素。
CSS提供了很多种可能性。有时,我听过float来解决问题,有时使用position:absolute,有时甚至会通过添加margin或者padding属性这种使代码变得比较脏的方式来达到目的。

我一点也不喜欢上面这些解决方案。浮动仅仅是顶部对齐并且需要手动清除浮动。绝对定位让元素脱离文档流,所以他们不再影响他们的周边元素。而使用固定值的外边距和内边距会出现细微的差别。

但是这里还有另外一种玩法:vertical-align。我认为他值得更多的赞誉。OK,技术上来说使用vertical-align布局是一种hack的手段,因为他并不是因为实现我们上面那种需求发明的。尽管如此,你也可以在不同的上下文中使用vertical-align来灵活细致的摆放元素。不需要知道元素的大小。元素在文档流中,也能感知其他元素尺寸的改变。这些都让vertical-align变成了更有价值的选项。

独特的vertical-align

但是,vertical-align有时也是个卑鄙小人。使用它工作也会使人有点沮丧。在使用它工作时,这里会有一些令人难以理解的规则。例如,它可能会发生这样的情况,一个元素改变了他自己的vertical-align的值,而他的位置却一点都没有改变,改变的却是同行的其他元素。我仍然会一次一次被拉入这种奇怪的现象,使我苦恼。

不幸的是,大多数资料都讲述的比较浅显。特别是,当我们想使用vertical-align布局时。他们对最基本的属性进行了介绍并且在简单的例子中解释元素是如何在一行中进行摆放的。但是,他们并没有去解释它令人感到奇怪的部分。

所以,我的目标是使自己彻底弄清楚vertical-align到底是怎么回事。最后消解我疑问的是W3C's的CSS specifications和标准中演示的一些例子。答案就在文章中。

所以,让我们来解决掉这场关于规则的游戏吧。

使用vertical-align的要求

vertical-align被用于inline-level elements。这些元素的display属性如下

  • inline

  • inline-block

  • inline-table(这篇文章中,并不考虑这种情况)

基本的inline元素都是标签裹着文字。

inline-block元素:是在行内中的块级元素。他们可以有宽度和高度(通常情况下,这取决于他们的内容)。同样也有padding,margin,border。

行内级元素在一行中一个挨着一个。一旦,这些元素超出了他们的所在行,一个新行便会建立在它下方。这里的每一行就叫做line box。每一行不同尺寸的元素意味着line box不同的高度。在下面的例子中红线代表line box的顶部和底部。

line box寻找我们放置元素的轨迹。在这些line boxes里面vertical-align属性负责摆放单独的元素。所以什么样的元素被用来对齐了呢?

关于基线和边界线

在一行中垂直摆放元素,最关键的参考点是参与的元素的基线。在一些例子中,被关闭盒子的顶部和底部也变得非常重要。让我们一起看看基线和外边缘是如何参与到不同类型的元素中的。

Inline Element

clipboard.png

这里有三行挨着的文字。顶部和底部的边缘线是行高的边缘线,它们都是红线。字体的高度被绿线包裹着。蓝色的线代表基线。在最左边,文字的行高和字体本身的高度一样高。绿线和红线发生了重叠。在中间,行高是字体大小的2倍高。在最右边,行高是字体大小的一半高。

inline element的外边缘的位置,取决于他们的行高的顶部与底部边缘。如果行高的高度比字体的高度小。那么,红色的外边缘的位置就如上面提到的一样。

inline element的基线,在字符放置的位置。在图中,用蓝线表示。初略的讲,基线的位置一般是在字体高度一半以下的位置。可以看看W3C的标准对它的定义,detailed definition

Inline-Block Element

clipboard.png

从左边到右边的图你可以看到:最左,一个在in-flow内的行内块级元素。中间,一个在文档流中的inline-block元素并且带有overflow: hidden属性的元素。最右,不在文档流中的inline-block元素(但是内容区域有高度)。margin的边界线指向红线,border是黄色的部分,padding是绿色的部分,内容区域是蓝色的部分。每一个inline-block元素的基线都用蓝线表示。

inline-block元素的外边缘 是它的margin-box的顶部和底部。在图上,用红线来表示。

inline-block元素的基线 依赖于元素是否是文档流中的元素。

  • 假设内容在文档流中,inline-block元素的基线是普通流中的最后一个内容元素的基线(最左的例子)。对于最后一个元素的基线根据它自己的规则来找寻。

  • 假设内容在文档流中,但是元素有overflow属性除了属性值为visible这种情况,基线在margin-box的底部(最中间的例子)。所以,这等同于在inline-block元素的底部边缘。

  • 假设内容不在文档流中,同样的,margin-box的底部边缘也是基线的位置所在。(例子在最右边)

Line Box

clipboard.png

你已经看到了上面元素的放置。这次,我画了line box的text box顶部和底部的线(如图中的绿线,更多内容看下文),基线如图中蓝色的线。我在文字元素上加了灰色的背景进行强调。

line box的顶部边缘与最顶部元素的顶线对齐,底部边缘与最底部元素的底线对齐。图中红线代表的就是line box。

line box的基线是多种多样的

“CSS2.1 并没有对line box基线的位置有明确的定义。-W3C Specs

当使用vertical-align工作时,这可能是最令人迷惑的部分。这意味着,基线的位置改变是为了满足像垂直对齐和降低line box的高度这样的需求的。也就是说,在方程中,这是一个自由的参数。

因为line box的基线是不可见的,因此它的位置并不会那么明显。但是,你可以容易的使它变得可见。只需要在一行的开头加上一个字母,就像我在图中添加的字母"x"。如果这个字母并没排在一条直线上,那么x坐落的位置就是基线的位置。

在line box基线的周围有text box。text box可以简单的被认为是在line box中的行内元素。它的高度等同于它的父元素的文字大小。因此,text box仅仅只是围绕着line box的非格式化文字。这个盒子在上图中是指向绿线的位置。text box与基线的位置相关联,它随着基线而移动。

哦,现在是最难的部分。现在,我们把知道的所有事,都表现在了上图中。让我们快速的总结最重要的部分:

  • 这里有一个区域被叫做line box。这是元素垂直对齐的区域。它有一个基线和一个text box以及顶部与底部边缘。

  • 这里有inline-level元素。他们是被对齐的对象。他们有基线以及顶部与底部边缘。

Vertical-Align的值

通过使用vertical-align的参考点,也就是上文提到的基线和边界线,将元素放置在合适的位置。

相对于line box的基线放置元素的基线

clipboard.png

  • baseline: 元素的基线刚好放置在line box基线的上。

  • sub: 元素的基线放置在line box基线的下面。

  • super: 元素的基线放置在line box的基线的上面。

  • <percentage> 元素的基线移动相对于line box的基线通过相对于line-height的百分比的值来移动。

  • <length>: 元素的基线相对于line box的基线通过绝对值的大小进行移动。

相对于line box的基线放置元素的外边缘

clipboard.png

  • middle: 元素的顶部与底部边缘的中点相对于line box的基线移动x-height的一半的位置对齐。

相对于line box的text box放置元素的外边缘

clipboard.png

  • text-top: 元素的顶部边缘被放置在line box的text box的顶部边缘

  • text-bottom: 元素的底部边缘被放置在line box的text box的底部边缘

相对于line box的外边缘放置元素的外边缘

clipboard.png

  • top: 元素的顶部边缘被放置在line box的顶部边缘

  • bottom: 元素的底部边缘被放置在line box的底部边缘

在W3C标准中的定义

Vertical-Align的行为就如它的表现一样

现在,我们可以在具体的例子中看垂直对齐。特别是,处理一些比较容易出现差错的情况。

放置icon在中间

这是一个一直以来都令我烦恼的事情:我有一个icon我想将它放置在一行文字的中间。只只仅仅将icon设置vertical-align: middle,看起来似乎不是一个安全的方法。看下面的例子。

clipboard.png

<!-- left mark-up -->
<span class="icon middle"></span>
Centered?

<!-- right mark-up -->
<span class="icon middle"></span>
<span class="middle">Centered!</span>

<style type="text/css">
  .icon   { display: inline-block;
            /* size, color, etc. */ }

  .middle { vertical-align: middle; }
</style>

这里也是上面这个例子,不过,这次我加了一些你从上文了解到的辅助线,

clipboard.png

这有助于帮我们理解。因为在左边的文字并没有对齐元素,而是被放置在基线所处的位置。通过使用vertical-align:middle来对齐盒子,我们将只是将盒子放置在了小写字母的中间。所以,出头部分的字母出现在顶部。

在右边,我们将全部文字区域放置在垂直方向的中点。通过移动text的基线到line box的基线的下方来实现。结果是文字完美的居中于紧挨着的icon。

移动line box的基线

当我们使用vertical-align工作时,容易遇到的一个共同的问题:line box的基线会被在同一行中所有元素影响。让我们猜一猜,一个元素有可能在这样被对齐的,通过移动line box的基线。因为大多数垂直对齐(除了顶部和底部)都是相对于基线,这也会导致在同一行的的所有元素的位置都会被调整。

一些例子:

  • 如果在一行中,有一个高的元素占据了整行的高度,vertical-align对它不会有任何影响。在它的顶部上面与底部下面没有任何多余的空间可以移动它。为了实现它相对于line box基线的对齐,line box的基线必须移动。矮一点的盒子设置了vertical-align: baseline。在左边,高盒子根据text-bottom对齐。在右边,根据text-top对齐。你可以看到矮盒子的随着基线位置的改变而改变。

clipboard.png

<!-- left mark-up -->
<span class="tall-box text-bottom"></span>
<span class="short-box"></span>

<!-- right mark-up -->
<span class="tall-box text-top"></span>
<span class="short-box"></span>

<style type="text/css">
  .tall-box,
  .short-box   { display: inline-block;
                /* size, color, etc. */ }

  .text-bottom { vertical-align: text-bottom; }
  .text-top    { vertical-align: text-top; }
</style>

通过其他vertical-align的值对齐高的元素,也会出现和上面的例子相同的行为。

  • 同样的给它设置vertical-align的值分别设为leftbottom移动基线。而这很奇怪,因为基线本不应该参与。

<!-- left mark-up -->
<span class="tall-box bottom"></span>
<span class="short-box"></span>

<!-- right mark-up -->
<span class="tall-box top"></span>
<span class="short-box"></span>

<style type="text/css">
  .tall-box,
  .short-box { display: inline-block;
               /* size, color, etc. */ }

  .bottom    { vertical-align: bottom; }
  .top       { vertical-align: top; }
</style>

clipboard.png

<!-- left mark-up -->
<span class="tall-box bottom"></span>
<span class="short-box"></span>

<!-- right mark-up -->
<span class="tall-box top"></span>
<span class="short-box"></span>

<style type="text/css">
  .tall-box,
  .short-box { display: inline-block;
               /* size, color, etc. */ }

  .bottom    { vertical-align: bottom; }
  .top       { vertical-align: top; }
</style>
  • 在一行中,放置两个大体积元素并且移动基线垂直对齐他们以此满足同时对齐。line box的高度被调整(左边的例子)。添加第三个元素,它并有跑到line box的边缘因为它的对齐属性,既没有影响line box的高度,也没有影响line box基线的位置(中间的例子)。如果它真的跑到了line box的边缘,我们的前两个盒子会被往下推(最右的例子)。

clipboard.png

<!-- left mark-up -->
<span class="tall-box text-bottom"></span>
<span class="tall-box text-top"></span>

<!-- mark-up in the middle -->
<span class="tall-box text-bottom"></span>
<span class="tall-box text-top"></span>
<span class="tall-box middle"></span>

<!-- right mark-up -->
<span class="tall-box text-bottom"></span>
<span class="tall-box text-top"></span>
<span class="tall-box text-100up"></span>

<style type="text/css">
  .tall-box    { display: inline-block;
                 /* size, color, etc. */ }

  .middle      { vertical-align: middle; }
  .text-top    { vertical-align: text-top; }
  .text-bottom { vertical-align: text-bottom; }
  .text-100up  { vertical-align: 100%; }
</style>

解开Vertical-Align的神秘面纱

当你知道了规则,这就没那么复杂了。如果vertical-align并没有按照你想的那样行动,问自己两个问题:

  • line box的顶部与底部边缘和基线在哪里?

  • inline-level元素的顶部与底部边缘和基线在哪里?

这将解决你的问题。

查看原文

赞 10 收藏 10 评论 0

认证与成就

  • 获得 4 次点赞
  • 获得 7 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-07-05
个人主页被 349 人浏览