第十章:Javascript子集和扩展

优质
小牛编辑
130浏览
2023-12-01

本章讨论javascript的集和超集,其中子集的定义大部分处于安全考虑。只有使用这门语言的一个安全的子集编写脚本,才能让代码执行的更安全、更稳定。ECMScript3标准是1999年版本的,10年后,ECMAScript5规范的更新,由于ECMAScript标准规范是允许对其做任何扩充的,伴随着Mozilla项目的改进,Firefox1.0、1.5、2.3.和3.5版本中分别发布了javascript1.5、1.6、1.7、1.8、1.81版本,这些javascript的扩展版本已经融入到ECMAScript5中,还有很多特性是非标准的,但这些特性很有可能在将来融入到ECMAScript版本中。

由于Firefox是基于一个名叫Spidermonkey的JavaScript引擎(Firefox的JavaScript引擎有很多种,"猴"系,Spidermonkey(用于在firefox 1.0-3.0)),firefox浏览器可以支持这些扩展特性。但由于这些语言特性是非标准的,本章的内容对于那些需要调试浏览器兼容性的开发者来说可能帮助不大。我们在本章对它们作必要的讲述是基于几点考虑:

  • 它们的确很强大
  • 它们有可能在未来成为标准
  • 它们可用来写Firefox扩展插件
  • 它们可用在服务器端的JavaScript编程

在简单介绍JavaScript语言的子集之后,本章后面会开始介绍语言的扩展部分。由于这些扩展毕竟不是标准,因此这里只是一个指南形式的描述。

1.javascript的子集

大多数语句都支持他们的子集,用以安全的执行不可信的第三方代码。这里有一个很有趣的子集,定义这个子集的原因有些特殊。我们首先来看这个有趣的子集,然后再讨论安全的语言子集。

i.精华:

Douglas Crockford曾经写过一本很薄的书《JavaScript: The good parts》(O'reily 出版社)专门介绍JavaScript中值得发扬光大的精华部分。这个语言子集的目标是简化这门语言,规避掉语言中的怪癖、缺陷部分,使编程更轻松、程序更健壮。Douglas Crockford是这样介绍他的动机的:

大多数编程语言都有精华部分和鸡肋部分,我发现如果只使用精华部分而避免使用鸡肋的部分,我可以成为一名更好的程序员。

他提炼出的子集部分不包含with和continue语句以及eval()函数。他提倡使用函数定义表达式而不是函数定义语句来定义函数。循环体和条件分支都使用花括号括起来,不允许在循环体和条件分支中只包含一条语句时省略花括号,任何语句只要不是以花括号结束都应当使用分号作结尾。

由于JavaScript并不包含块级作用域,Crockford为我们提炼出的子集部分对var语句做了限制,var语句只能出现在函数体的顶部,并要求程序员将函数内所有的变量声明写在一个单独的var语句中。子集中禁止使用全局变量,但这个限制只是编程约定,并不是真正的语言上的限制。

它的网站http://www.jslint.com/是一个代码检测工具网站,提供了很多选项用来对代码的一致性进行增强。除了保证我们编写的代码保证子集推荐的特性之外,还对代码风格进行了约定,比如缩进等

在Crock ford出这本书 的时候,ECMAScript5的严格模式还没有出来,所以,在ECMAScript5严格模式中,很大一部分都和它有同样的限制。随着ECMAScript5广泛采用,.jslint.工具在选中The good parts时,程序必须包含use strict代码。

ii.子集的安全性

利用子集所追求的目标,我们可以设计出更美且提升程序员效率的javascript代码,为了让javascript安全的运行,我们必须移除一些javascript特性,使其安全子集的运行:

  • 禁止使用eval()和Function()构造函数,因为它们可以执行任意代码,而且JavaScript无法对这些代码作静态分析。
  • 禁止使用this关键字
  • 禁止使用with语句,因为with语句增加静态代码检查难度。
  • 禁止使用某些全局变量,在客户端javascript中,浏览器窗口对象当做全局对象,也有双重身份,因此代码中不能有window对象。同样的,document对象定义操作整个页面的方法,将document对象交给一段不受信任的代码会存在隐患。所有安全子集定义了可以用来操作整个页面内容的方法,第一种方法是:完全禁止掉它们,并定义一组api用以分配它们做有效的页面限制访问。第二章方法,在代码所运行的容器里定义一个只对外面提供安全标准的DOM api.
  • 禁止使用某些属性和方法,这些属性和方法包括arguments对象的两个属性caller和callee(甚至在某些子集中干脆禁止使用arguments对象)、函数的call()和apply()方法、以及constructor和prototype两个属性。

有一些限制,比如禁止使用eval()和with语句,并不会对开发者带来额外负担,毕竟这些特性本来就很少在JavaScript编程中用到。另外一些限制规则,比如使用方括号对属性进行存取的限制则对开发造成诸多不便,这时就有代码转换器的用武之地了。比如,转换器会自动将使用方括号的代码转换为函数调用的代码,以便能够对它执行运行时检查。有了这种代码转换,我们是可以安全的使用this关键字的。

有一些安全子集已经实现了,我们只做一些简要介绍

  • ADsafe(ADsafe是第一个提出的安全子集,它的提出者是Douglas Crockford,他也定义了《JavaScript: The good parts》(O'reily 出版社)子集)ADsafe只包含静态检查,使用http://www.jslint.com/作为检验器。这个工具禁止大部分全局变量。并定义了一个ADsafe变量,它提供了一组可以安全使用的API,包含一些特殊的DOM方法。
  • dojox.secure (dojo工具包 http://dojotoolkit.org/)发布了名为dojox.secure的子集扩展。和adsafe一样,dojox.secure也是静态检查,静态检查受限子语言子集范围内,但它和adfafe又不一样,它允许使用标准的DOM api.同时,它又包含一个javascript实现的检查器,我们用它对不可信的第三方代码执行运行时前的动态检查。
  • Caja(Caja是西班牙语的意思:“沙盒”详情:http://code.google.com/p/google-caja/ )是Google发布的开源安全子集。Caja定义了两个语言子集。Cajita(“小沙盒”)是一个与ADsafe和dojox.secure类似的严格子集。Valija(“手提箱”或“行李箱”)则是一个范围更广的语言子集,更接近于ECMAScript 5的严格模式(不包含eval())。Caja本身也是一个编译器的名字,这个编译器可以将一段网页内容(HTML、CSS和JavaScript代码)转换为一个安全的模块,这个模块可以放心的引用至页面中而不会对其他模块产生影响Caja是开OpenSocial API的一部分,已被Yahoo率先采用。例如http://my.yahoo.com代码中,所有的模块都遵照了Caja规范。
  • FBJS FBJS是JavaScript语言的变种,已经被Facebook所采用,用以在用户个人资料页嵌入不可信的第三方代码。FBJS依赖代码转换器来保证代码的安全性,转换器同样提供运行时检查,以避免通过this关键字去访问全局对象,并且对所有的顶层标识符进行重命名,给它们增加了一个标识模块的前缀,正是因为这种重命名,任何对全局变量以及其他模块的成员变量的存取操作都无法正常进行了。此外,任何对eval()的调用也会因为eval函数名被重新命名而无法执行。FBJS模拟实现了一个DOM API的安全子集。
  • Web Sandbox(http://websandbox.livelabs.com/)定义了JavaScript的一个更宽泛的子集,包含HTML和CSS,它的代码重写规则非常激进,有效地实现了一个安全的JavaScript虚拟机,针对不安全的JavaScript顶层代码作处理。

2.常量和局部变量

下面讨论语言的扩展,javascript1.5后续版本中可以使用const关键字定义常量,常量重新赋值会失败但不会报错。对常量重复声明会报错。

        const pi = 3.14150926; //定义一个常量并赋值
        pi = 4;
        console.log(pi); //=> 3.14150926 赋值被忽略
        const pi = 4; //=> Identifier 'pi' has already been declared 重新声明报错
        var pi = 4; //=> Identifier 'pi' has already been declared

const和var相类似,因为JavaScript没有块级作用域,所用常量被提前至函数定义的顶部。

一直以来,JavaScript的变量缺少块级作用域的支持被普遍认为是javascript的短板,javascript1.7针对这个缺陷增加了关键字let。关键字const一直都是JavaScript的保留字(没有使用),因此现有的代码不必作任何改动就可以增加常量,关键字let并不是保留字,javascript1.7及以后的版本才能识别。let有四种使用方式:

  • 可以作为变量声明,和var一样;
  • for或for/in循环中,var关键字的替代方案;
  • 在语句块中定义一个变量并显式指定它的作用域;
  • 定义一个在表达式内部作用域中的变量,这个变量只在表达式内可见。

使用let最简单的方式就是替换程序中的var,通过var声明变量的函数内都是可用的,而通过let声明的变量则只属于就近的花括号括起来的那块语句(包括嵌套的语句)。比如在循环体内使用let声明变量,那么这个循环之外是不可用的,如下代码:

        function oddsum(n) {
            let total = 0;
            let result = [];
            for (let = 0, x < n; x++;) { //x只在循环内有定义
                let odd = 2 * x - 1;
                total += odd;
                result.push(total);
            }
            return total;
        }
        oddsum(5)

我们注意到,这段代码中let还替代了for循环中的var。这时通过let创建的变量的作用域仅限于循环体、循环条件判断逻辑和自增操作表达式。同样,可以这样在for/in(以及for each,参照11.4.i)循环中使用let:

        o = {
            x: 1,
            y: 2
        };
        for (let p in o) console.log(p); //=>x和y
        for each(let v in o) console.log(v); //=> 1和2
        console.log(p) //错误:p没有定义

在声明语句使用let和循环初始化使用let有着有趣的区别,对于前者来说,变量初始化便打算是在变量作用域计算的。但后者来说,变量的初始是在作用域之外的。当出现两个同名变量时要格外注意:

        let x = 1;
        for (let x = x + 1; x < 5; x++)
            console.log(x) //=>2,3,4
            {
                let x = x+1;//x没有定义,因此相加nan
                console.log(x)
            }

另外的一些示例:

        let x = 1,
            y = 2;
        let (x = x + 1, y = x + 2) { // 注意这里的写法
            console.log(x + y); // 输出 5
        };
        console.log(x + y); //输出3

let语句中的变量初始化表达式并不是这个语句块的一部分,并且是在作用域外部被解析的,理解这一点至关重要。

let关键字的最后一种用法是let语句写法的一个变体,其中有一个括号括起来的变量列表和初始化,紧跟着是一个表达式而不是语句块。我们把这种写法叫做let表达式,上面的代码可以写成这样:

let x=1, y=2;
console.log(let (x=x+1,y=x+2) x+y); // 输出 5

3.解构赋值

Spidermonkey系javascript1.7实现了一种混合式赋值,我们称之为“解构赋值”。你可能在python或者ruby中接触过此概念,等候右侧是一个数组或对象(解构话的值),指定左侧一个或多个变量的语法和右侧数组对象直接量和语法格式一致。

发生解构赋值时,右侧的数组或对象中的一个/多个值就会被提取出来(解构),并赋值给左侧相应的变量名。除了常规赋值运算外,解构赋值还用以初始化用var和let新生命的变量。

和数组配合使用时,解构赋值是一种写法简单且有极其强大的功能。特别是在函数返回一组结果的时候解构赋值就显得非常有用。然而当配合对象或者嵌套对象一起使用时,解构赋值变得更加复杂且容易搞混。下面的例子展示了简单的和复杂的解构赋值:

        let [x, y] = [1, 2]; // 等价于 let x=1,y=2
        [x, y] = [x + 1, y + 1]; //等价于 x = x+1,y=y+1
        [x, y] = [y, x]; // 交换两个变量的值
        console.log([x, y]); //输出 [3,2]

当函数返回一组结果时,解构赋值方式将大大简化程序代码:

         // 将 [x,y] 从笛卡尔(直角)坐标转换为 [r,theta] 极坐标
        function polar(x, y) {
                return [Math.sqrt(x * x + y * y), Math.atan2(y, x)];
            }
            // 将极坐标转换为笛卡尔坐标

        function cartesian(r, theta) {
            return [r * Math.cos(theta), r * Math.sin(theta)];
        }
        let [r, theta] = polar(1.0, 1.0); // r=Math.sqrt(2), theta=Math.PI/4
        let [x, y] = cartesian(r, theta); // x=1.0, y=1.0

由于解构代码方式学习过去生疏难懂,此处略去内容,在javascript核心语法完成后我将把本书的扫描PDF文档分享给大家参考。

4.迭代

mozilla的javascript扩展引入了一些新的迭代机制,包括for each循环和python风格的迭代器(iterator)和生成器(generator)。本文小节中将逐步介绍。

i.for/each循环

for/each循环是由e4x规范(ecmascript for xml)定义的一种新的循环语句。e4x是语言的扩展,它允许javascript程序中直接出现xml标签,并定义了操作xml数据的语法和api。浏览器大都没有实现e4x,但是mozilla javascript 1.6(随着firefox 1.5发布)是支持e4x的。本节我们只对for/each作讲解,并不会涉及到xml对象。关于e4x的剩余内容请参照11.7节:

for each循环和for/in循环非常类似。但for each并不是遍历对象的属性,而是对属性的值作遍历:

        let o = {
            one: 1,
            two: 2,
            three: 3
        }
        for (let p in o) console.log(p); // for/in: 输出 'one', 'two', 'three'
        for each(let v in o) console.log(v);

当使用数组时,for/each循环遍历的元素(而不是索引),它通常按照数值顺序枚举它们,但实际上这并不是标准化或必须的:

注意,for/each循环并不仅仅针对数组本身的元素作遍历,它也会遍历数组中所有可枚举属性,包括继承来的可枚举的方法。因此,通常并不推荐for/each循环和数组一起使用。在ECMAScript5 之前的 javascript 版本中是可以这样用的,因为自定义属性和方法不可能设置为可枚举的。

        a = ['one', 'two', 'three'];
        for (let p in a)
            console.log(p); //=>0,1,2 数组索引:
        for each(let v in a)
         console.log(v); //=>'one', 'two', 'three' 数组元素

ii.迭代器

迭代器是一个对象,这个对象允许对它的值的集合作遍历,并保持任何必要的状态以便能够跟踪到当前遍历的“位置”。

迭代器必须包含next()方法,每一次对next()调用都返回集合中的下一个值。比如下面的counter()函数返回了一个迭代器,这个迭代器每次调用next()都会返回连续递增的整数。需要注意的是,这个函数利用闭包的特性实现了计数器状态的保存:

        function counter(start) {
            let nextValue = Math.round(start); // 表示迭代器状态的一个私有成员
            return {
                next: function() {
                    return nextValue++;
                }
            }; // 返回迭代器对象
        }
        let serialNumberGenerator = counter(1000);
        let sn1 = serialNumberGenerator.next(); // 1000
        let sn2 = serialNumberGenerator.next(); // 1001

迭代器用于有限的集合时,当所有的值都遍历完成没有多余的值可迭代时,再调用next()方法会抛出stopiteration。stopiteration是javascript 1.7中的全局对象的属性。它是一个普通的对象(它自身没有属性),只是为了终结迭代的目的而保留的一个对象。注意,实际上,stopiteration并不是像typeerror()和rangeerror()这样的构造函数。比如,这里实现了一个rangeiter()方法,这个方法返回了一个可以对某个范围的整数进行迭代的迭代器:

         // 这个函数返回了一个迭代器,它可以对某个区间内的整数作迭代
        function rangeIter(first, last) {
                let nextValue = Math.ceil(first);
                return {
                    next: function() {
                        if (nextValue > last) throw StopIteration;
                        return nextValue++;
                    }
                };
            }
            // 使用这个迭代器实现了一个糟糕的迭代.
        let r = rangeIter(1, 5); // 获得迭代器对象
        while (true) { // 在循环中使用它
            try {
                console.log(r.next()); // 调用  next() 方法
            } catch (e) {
                if (e == StopIteration) break; // 抛出 StopIteration 时退出循环
                else throw e;
            }
        }

由于扩展迭代代码方式学习生疏难懂(包含生成器,生成器表达式,迭代器,数组推导等),此处略去内容,在javascript核心语法完成后我将把本书的扫描PDF文档分享给大家参考。

5.函数简写

对于简单的函数,JavaScript 1.8引入了一种简写形式:“表达式闭包”。如果函数只包含一个表达式并返回它的值,关键字return和花括号都可以省略,并将待计算的表达式放在参数列表之后,这里有一些例子:

let succ = function(x) x + 1,yes = function() true,no = function() false;

这只是一种简单的快捷写法,用这种形式定义的函数其实和带花括号和关键字return的函数完全一样,这种快捷写法更适用于当给函数传入另一个函数的场景,比如:

data.sort(function(a, b) b - a);
         // 定义一个函数,用以返回数组元素的平方和
        let sumOfSquares = function(data)
         Array.reduce(Array.map(data, function(x) x * x), function(x, y) x + y);

6.多catch从句

在javascript1.5中,try/catch语句已经可以使用多catch从句了,在catch从句的参数中加入关键字if以及一个条件判断表达式:

        try {
            // 这里可能会抛出多种类型的异常
            throw 1;
        } catch (e
            if e instanceof ReferenceError) {
            // 这里处理引用错误
        } catch (e
            if e === "quit") {
            // 这里处理字符串是“quit”的情况
        } catch (e
            if typeof e === "string") {
            // 处理其他字符串的情况
        } catch (e) {
            // 处理余下的异常情况
        } finally {
            // finally从句正常执行
        }

当产生了一个异常时,程序将会尝试执行每一个catch从句。catch从句中的参数即是这个异常,执行到catch的时候会它的计算条件表达式。如果条件表达式计算结果为true,则执行当前catch从句中的逻辑,同时跳过其他的catch从句。如果catch从句中没有条件表达式,程序会假设它包含一个if true的条件,如果它之前的catch从句都没有被激活执行,那么这个catch中的逻辑一定会执行。如果所有的catch从句都包含条件,但没有一个条件是true,那么程序会向上抛出这个未捕获的异常。注意,因为catch从句中的条件表达式已经在括号内了,因此也就不必像普通的条件句一样再给他包裹一个括号了。

注意:

<script type="text/javascript; version=1.5" >

7.e4x:ECMAScript for XML

“ECMAScript for XML”简称E4X,是JavaScript的一个标准扩展(注:E4X是由ECMA-357规范定义的。可以从这里查看官方文档:http://www.ecma-international.org/publications/standards/Ecma-357.htm),它为处理XML文档定义了一系列强大的特性。Spidermonkey 1.5和Rhino 1.6已经支持E4X。由于多数浏览器厂商还未支持E4X,因此E4X被认为是一种基于Spidermonkey或Rhino引擎的服务器端技术。

E4X将XML文档(元素节点或属性)视为一个XML对象,将XML片段视为一个紧密相关的XML列表对象。本节会介绍创建和使用XML对象的一些方法。XML对象是一类全新的对象,E4X中定义了专门的语法来描述它(接下来会看到)。我们知道,除了函数之外所有标准的JavaScript对象的typeof运算结果都是“object”。正如函数和原始的JavaScript对象有所区别一样,XML对象也和原始JavaScript对象不同,对它们进行typeof运算的结果是“xml”。在客户端JavaScript中(参照第15章),XML对象和DOM(文档对象模型)对象没有任何关系,理解这一点非常重要。E4X标准也针对XML文档元素和DOM元素之间的转换作了规定,这个规定是可选的,Firefox并没有实现它们之间的转换。这也是E4X更适用于服务器端编程的原因。

本小节中我们会给出一个E4X的快速入门教程,而不会作更深入的讲解。XML对象和XML列表对象的很多方法本书中并未介绍。在参考手册部分也不会对其作讲解,如果读者希望进一步了解E4X,可以参照官方文档。

E4X只定义了很少的语言语法。最显著的当属将XML标签引入到JavaScript语言中。可以在JavaScript代码中直接书写XML标签直接量,比如:

         // 创建一个XML对象
        var pt = <periodictable>
            <element id = "1"> <name> Hydrogen </name></element>
            <element id = "2"> <name> Helium </name></element>
            <element id = "3"> <name> Lithium </name></element>
            </periodictable>;

            // 给这个表格添加一个新元素
         pt.element += <element id = "4"> <name> Beryllium </name></element> ;

XML直接量语法中使用花括号作为变量输出,我们可以在XML中嵌入JavaScript表达式。例如,这里是另外一种创建XML元素的方法:

pt = <periodictable></periodictable>; // 创建一个新表格
var elements = ["Hydrogen", "Helium", "Lithium"]; // 待添加的元素
// 使用数组元素创建XML元素
for(var n = 0; n < elements.length; n++) {
pt.element += <element id={n+1}><name>{elements[n]}</name></element>;

除了使用直接量语法,我们也可以将字符串解析成XML,下面的代码为上段代码创建的节点增加了一个新元素:

pt.element += new XML('<element id="5"><name>Boron</name></element>');

当涉及到XML片段的时候,使用XMLList()替换XML():

pt.element += new XMLList('<element id="6"><name>Carbon</name></element>' +
'<element id="7"><name>Nitrogen</name></element>');

E4X提供了一些显而易见的语法用以访问所创建的XML文档的内容:

var elements = pt.element;      // 得到element列表
var names = pt.element.name;    //得到所有的name标签
var n = names[0];           //"Hydrogen"(氢),name的第零个标签的内容

E4X同样为操作XML对象提供了语法支持,点点(..)运算符是“后代运算符”(descendant operator),可以用它替换普通的点(.)成员访问运算符:

// 另一种得到<name>标签列表的方法
var names2 = pt..name;

E4X甚至定义了通配符运算:

// 得到所有<element>标签的所有子节点
// 这也是得到所有<name>标签的另外一种方法
names3 = pt.element.*;

E4X中使用字符@来区分属性名和标签名(从XPath中借用过来的语法)。比如,你可以这样来获得一个属性:

// “氮”的原子序数是多少
var atomicNumber = pt.element[1].@id;

可以使用通配符来获得属性名@*,

// 获得所有的<element>标签的所有属性
var atomicNums = pt.element.@*;

E4X甚至包含了一种强大且及其简洁的语法用来对列表进行过滤,过滤条件可以是任意谓词表达式:

// 对所有的element元素组成的列表进行过滤
// 过滤出那些id属性小于3的元素
var lightElements = pt.element.(@id < 3);

// 对所有的element元素组成的列表进行过滤
// 过滤出那些name以B开始的元素。
// 然后得到过滤后元素的<name>标签列表
var bElementNames = pt.element.(

本章的11.4.i中讲到for/each循环是非常有用的,但在E4X标准中对for/each循环有了新的定义,可以用for/each来遍历XML标签和属性列表。for/each和for/in循环非常类似,for/in循环用以遍历对象的属性名,for/each循环用以遍历对象的属性值:

// 输出元素周期表中的每个元素名
for each (var e in pt.element) {
console.log(e.name);
}

// 输出每个元素的原子序数
for each (var n in pt.element.@*) console.log(n);

E4X表达式可以出现在赋值语句的左侧,可以用它来对已存在的标签和属性进行修改或添加新标签或属性:

// 修改氢元素的<element>标签,给它添加一个新属性
// 像下面这样添加一个子元素:
//
// <element id="1" symbol="H">
//   <name>Hydrogen</name>
//   <weight>1.00794</weight>
// </element>
//
pt.element[0].@symbol = "H";
pt.element[0].weight = 1.00794;

通过标准的delete运算符也可以方便地删除属性和标签:

delete pt.element[0].@symbol; // 删除一个属性
delete pt..weight;      //删除所有的<widget>标签

我们可以通过E4X所提供的语法来进行大部分的XML操作。E4X同样定义了用于调用能够XML对象的方法,例如,这里用到了insertChildBefore()方法:

pt.insertChildBefore(pt.element[1],
<element id="1"><name>Deuterium</name></element>);

E4X中是完全支持命名空间的,它为使用XML命名空间提供了语法支持和API支持:

// 声明默认的命名空间:default xml namespace = "http://www.w3.org/1999/xhtml";
// 这里是一个包含了一些svg标签的xhtml文档
d = <html>
<body>
 This is a small red square:
 <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">
     <rect x="0" y="0" width="10" height="10" fill="red"/>
 </svg>
</body>
</html>

// body元素和它的命名空间里的uri以及他的localName
var tagname = d.body.name();
var bodyns = tagname.uri;
var localname = tagname.localName;

// 选择<svg>元素需要多做一些工作,因为<svg>不在默认的命名空间中,
// 因此需要为svg创建一个命名空间,并使用::运算符将命名空间添加至标签名中
var svg = new Namespace('http://www.w3.org/2000/svg');
var color = d..svg::rect.@fill // "red"

(本文完,欢迎大家关注上一章:第九章:Javascript类和模块

由于时间和精力有限,在时间允许的情况下更新正则表达式章节和服务器端javascript(rhino校本化java/node异步i/o),下面的章节将围绕客户端javascript开展,欢迎朋友们关注。